Wednesday, December 22, 2010

Scanning Images to PDF

In this post I will show how to use Windows Image Acquisition (WIA) interface to scan images and convert them to PDF using converter from my previous post.

There is a nice intro article on WIA written by Scott Hanselman several years ago. Please read it, I referred to it several times over the years. There is also some information available on Wikipedia.

So what is WIA:

WIA is a Microsoft driver model and application programming interface (API) for Microsoft Windows Me and later Windows operating systems that enables graphics software to communicate with imaging hardware such as scanners, digital cameras and Digital Video-equipment. It was first introduced in 2000 as part of Windows Me, and continues to be the standard imaging device and API model through successive Windows versions. It is implemented as an on-demand service in Windows XP and later Windows operating systems.

If you are developing for Windows XP you will need to download this tool. It requires one time administrative installation on a client machine. I created a little batch file to install these files:

echo off

cls

echo Copying WIA Files ...

cd c:\

copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.chi c:\windows\help
copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.chm c:\windows\help
copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.dll c:\windows\system32

RegSvr32 WIAAut.dll

echo DONE!!!

If you are developing for Windows Vista or Windows 7 you need to know this:

“In Windows XP, WIA runs in the LocalSystem context. Because of the security ramifications of running a service as LocalSystem whereby a buggy driver or malicious person would have unrestricted access to the system, the WIA service in Windows Server 2003 and Windows Vista operates in the LocalService context. This can result in compatibility issues when using a driver designed for Windows XP.”

“Windows Vista has the WIA Automation library built-in. Also, WIA supports push scanning and multi-image scanning. Push scanning allows initiating scans and adjusting scanning parameters directly from the scanner control panel. Multi-image scanning allows you to scan several images at once and save them directly as separate files. However, video content support is removed from WIA for Windows Vista. Microsoft recommends using the newer Windows Portable Devices API.”

Now on to the hardware. Since in my project I needed to scan multiple pages at once, I used Fujitsu fi-5110C and newer model (works just as well) fi-6110. The problem is that in Windows XP for some reason these scanners don’t report correctly the “Empty Tray” status. So I had to assume some things for multi page scanning. The logic is simple:

- scan until no exception.
- when exception occurs checks if anything is scanned.
- if pages were scanned then simply exit.
- if no pages were scanned then report a problem.

Here is a snippet responsible for this:


// if we reached this point, then scanner is probably initialized. 
bool isTransferring = true;
foreach (string format in item.Formats)
{
while (isTransferring)
{
try
{
WIA.ImageFile file = (item.Transfer(format)) as WIA.ImageFile;
if (file != null)
{
Stream stream = new MemoryStream();
stream.Write(file.FileData.get_BinaryData() as Byte[], 0, (file.FileData.get_BinaryData() as Byte[]).Length);

// resetting stream position to beginning after data was written into it.
stream.Position = 0;
Bitmap bitmap = new Bitmap(stream);
images.Add(bitmap);
}
else
isTransferring = false; // something happend and we didn't get image
}
catch (Exception ex)
{
// most likely done transferring
// I was not able to find a way to pole scanner for paper feed status.
isTransferring = false;

// scanner's paper feeder was not loaded with paper.
if (images.Count() == 0)
throw new ImageScannerException("Scanner is not loaded with paper or not ready.");

}
}
}

That’s multi page scanning in Windows XP, unlike earlier statement from Wiki that it is supported only in Vista.

Another not so straight forward part is how to initialize the scanner. By default when you call WIA API to scan it loads manufacturers interface. In my case I wanted to save all the settings using internally developed user form and from then on pass these settings on to a scanner every time scanner is used.

I created a class to store scanner settings.

ScannerSettings:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace Bmec.ScanLibrary.Scan
{
public enum ScanningSize
{
Letter,
Legal
}

public enum Color
{
GrayScale=0,
Color=1,
BlackWhite=4
}

public class ScannerSettings: ISerializable
{
#region Fields
/// <summary>
/// Width of the scanned image.
/// Default is 8500.
/// </summary>
private double _width = 8.5;

/// <summary>
/// Height of the scanned image.
/// Default is 11000.
/// </summary>
private double _height = 11;

/// <summary>
/// Optical Resolution.
/// </summary>
private int _resolution = 120;

/// <summary>
/// Color setting, default is 0 - grayscale.
/// </summary>
private int _color = 0; // grayscale

/// <summary>
/// Horizontal Cropping.
/// Default is 0.5 in
/// </summary>
private double _horizontalCrop = 0.5; // if cropping required it can be set in here for horizontal.

/// <summary>
/// Vertical Cropping
/// Default is 0.5 in
/// </summary>
private double _verticalCrop = 0.5; // if cropping required it can be set in here for vertical.

/// <summary>
/// Standard scanning size
/// </summary>
private ScanningSize _size = ScanningSize.Letter;

#endregion //Fields

#region Constructors

/// <summary>
/// Default Constructor
/// </summary>
public ScannerSettings()
{

}

/// <summary>
/// Creates settings object for WIA scanner.
/// </summary>
/// <param name="size">Standard paper size.</param>
/// <param name="resolution">scanning resolution (e.g. for 300x300 pass 300).</param>
/// <param name="color">Color setting, default is gray scale.</param>
public ScannerSettings(ScanningSize size, int resolution, Color color)
{

Size = size;
_color = (int)color;
_resolution = resolution;
}

/// <summary>
/// Creates settings object for WIA scanner and resolution of 150x150 pixels.
/// </summary>
/// <param name="size">Standard paper size.</param>
public ScannerSettings(ScanningSize size)
{
Size = size;
}

/// <summary>
/// Creates customized settings for WIA scanner
/// </summary>
/// <param name="width">Scanner's sheet feed width.</param>
/// <param name="height">Scanner's sheet feed height.</param>
/// <param name="resolution">Optical resolution.</param>
public ScannerSettings(double width, double height, int resolution, double horizontalCrop, double verticalCrop)
{
Width = width;
Height = height;
_resolution = resolution;
_horizontalCrop = horizontalCrop;
_verticalCrop = verticalCrop;
}


#endregion //Constructors

#region Properties

/// <summary>
/// Color setting, default is 0 - grayscale.
/// </summary>
public int Color
{
get
{
return _color;
}
set { _color = value; }
}

/// <summary>
/// Height of the scanned image.
/// Default is 11 in.
/// </summary>
public double Height
{
get
{
return _height;
}
set
{
_height = value;
}
}

/// <summary>
/// Horizontal Cropping.
/// Default is 0.5 in.
/// </summary>
public double HorizontalCrop
{
get
{
return _horizontalCrop;
}
set
{
_horizontalCrop = value;
}
}

/// <summary>
/// Optical Resolution. Default is 120 pixels per inch.
/// </summary>
public int Resolution
{
get
{
return _resolution;
}
set
{
_resolution = value;
}
}

/// <summary>
/// Standard scanning size
/// </summary>
public ScanningSize Size
{
get
{
return _size;
}
set
{
_size = value;
Width = GetWidth(_size);
Height = GetHeight(_size);
}
}

/// <summary>
/// Vertical Cropping
/// Default is 0.5 in.
/// </summary>
public double VerticalCrop
{
get
{
return _verticalCrop;
}
set
{
_verticalCrop = value;
}
}

/// <summary>
/// Width of the scanned image.
/// Default is 8.5 in.
/// </summary>
public double Width
{
get
{
return _width;
}
set
{
_width = value;
}
}
#endregion //Properties

#region Methods
public static double GetWidth(ScanningSize size)
{
if (size == ScanningSize.Legal)
{
return 8.5;
}
else if (size == ScanningSize.Letter)
{
return 8.5;
}
else return 8.5;
}

public static double GetHeight(ScanningSize size)
{
if (size == ScanningSize.Legal)
{
return 14;
}
else if (size == ScanningSize.Letter)
{
return 11;
}
else return 11;
}
#endregion //Methods

#region ISerializable Members

/// <summary>
/// Constructor for serializer.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
public ScannerSettings(SerializationInfo info, StreamingContext context)
{

Color = info.GetInt32("Color");
Height = info.GetDouble("Height");
HorizontalCrop = info.GetDouble("HorizontalCrop");
Resolution = info.GetInt32("Resolution");
Nullable<ScanningSize> size = info.GetValue("Size", typeof(Nullable<ScanningSize>)) as Nullable<ScanningSize>;
Size = ((size != null)? size.Value : ScanningSize.Letter);
VerticalCrop = info.GetDouble("VerticalCrop");
Width = info.GetDouble("Width");
}

/// <summary>
/// Implementation for ISerializable.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Color", Color);
info.AddValue("Height", Height);
info.AddValue("HorizontalCrop", HorizontalCrop);
info.AddValue("Resolution", Resolution);
Nullable<ScanningSize> size = Size;
info.AddValue("Size", size);
info.AddValue("VerticalCrop", VerticalCrop);
info.AddValue("Width", Width);

}

#endregion
}
}

And then apply these settings in the following manner:

// setting properties (dimensions and resolution of the scanning.)
setItem(item, "6146", _scannerSettings.Color); // color setting (default is gray scale)
setItem(item, "6147", _scannerSettings.Resolution); //horizontal resolution
setItem(item, "6148", _scannerSettings.Resolution); // vertical resolution
setItem(item, "6149", _scannerSettings.HorizontalCrop); // horizontal starting position
setItem(item, "6150", _scannerSettings.VerticalCrop); // vertical starting position
setItem(item, "6151", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Width - _scannerSettings.HorizontalCrop))); // width
setItem(item, "6152", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Height - _scannerSettings.VerticalCrop))); // height

Another annoying window is selecting the scanner itself:

clip_image002

This is taken care by saving device id into local user settings and then reading it off:

#region Private Methods

private void setItem(IItem item, object property, object value)
{
WIA.Property aProperty = item.Properties.get_Item(ref property);
aProperty.set_Value(ref value);
}

private Device GetDevice(Settings settings)
{
Device device=null;
CommonDialogClass dialog = new CommonDialogClass();
if (String.IsNullOrEmpty(settings.DeviceId))
{
device = dialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true, false);
if (device != null)
{
settings.DeviceId = device.DeviceID;
settings.Save();
}
}
return device;
}

private Device GetDevice(string deviceId)
{
WIA.DeviceManager manager = new DeviceManager();
Device device=null;
foreach( DeviceInfo info in manager.DeviceInfos)
{
if(info.DeviceID == deviceId)
{
device = info.Connect();
break;
}
}
return device;
}
#endregion //Private Methods

Here is the rest of the code. Scanner initialization was a little tricky without fully understanding the WIA API. So I had to resort to several hacks and empirical methods :).

Below are the other two classes you will need to scan images.

ImageScanner:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bmec.ScanLibrary.Properties;
using System.Configuration;
using WIA;
using System.Drawing;
using System.IO;
using System.Security.Permissions;
using System.Security.Principal;

[assembly: System.Security.Permissions.FileIOPermission(SecurityAction.RequestMinimum, Unrestricted = true)]
namespace Bmec.ScanLibrary.Scan
{
public class ImageScanner
{
#region Fields
private Device _device=null;
private ScannerSettings _scannerSettings;
#endregion //Fields

#region Constructors

/// <summary>
///
/// </summary>
/// <exception cref="ImageScannerException">This exception can be thrown,
/// if any errors are present while initializing scanner.</exception>
public ImageScanner(ScannerSettings scannerSettings)
{
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

if (scannerSettings == null)
throw new ImageScannerException("Scanner Settings are not specified!");

_scannerSettings = scannerSettings;

Settings settings = new Settings();
try
{
// try automatically select scanner.
if (!String.IsNullOrEmpty(settings.DeviceId))
{
_device = GetDevice(settings.DeviceId);
}

// if didn't succeed try manually select scanner.
if (_device == null)
{
_device = GetDevice(settings);
}

}
catch (Exception ex)
{
throw new ImageScannerException("Scanner was not selected or is not available!\n"+ex.Message);
}

// if device is still null, then throw an error.
if (_device == null)
throw new ImageScannerException("Scanner was not selected or is not available!");

}

#endregion //Constructors

#region Public Methods

public void SetDevice()
{
Settings settings = new Settings();
_device = GetDevice(settings);
}

/// <summary>
/// This will scan images, but everything must be ready before doing this.
/// If device (scanner) is not ready an ImageScannerException will be thrown.
/// </summary>
/// <returns>Collection of scanned images.</returns>
public IEnumerable<Bitmap> Scan()
{
List<Bitmap> images = new List<Bitmap>();

if (_device == null)
throw new ImageScannerException("Scanner is not available! Please select a scanner and try again.");

WIA.Item item = null;
try
{
foreach (DeviceCommand command in _device.Commands)
{
item = _device.ExecuteCommand(command.CommandID);
}
}
catch (Exception ex)
{
// skip this
}

try
{
// if item is still not initialized, we'll try a different approach
if (item == null)
{
foreach (Item i in _device.Items)
{
foreach (DeviceCommand command in i.Commands)
{
item = _device.ExecuteCommand(command.CommandID);
}
}
}
}
catch (Exception ex)
{
// skip this
}

try
{
// if item is still null, we'll pick the first available
foreach (WIA.Item i in _device.Items)
{
item = i;
break;
}
}
catch (Exception ex)
{
//skip this
}

if(item == null)
throw new ImageScannerException("Scanner is not ready!\nPlease turn scanner on, feed paper into the scanner and try again.");
try
{
// setting properties (dimensions and resolution of the scanning.)
setItem(item, "6146", _scannerSettings.Color); // color setting (default is gray scale)
setItem(item, "6147", _scannerSettings.Resolution); //horizontal resolution
setItem(item, "6148", _scannerSettings.Resolution); // vertical resolution
setItem(item, "6149", _scannerSettings.HorizontalCrop); // horizontal starting position
setItem(item, "6150", _scannerSettings.VerticalCrop); // vertical starting position
setItem(item, "6151", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Width - _scannerSettings.HorizontalCrop))); // width
setItem(item, "6152", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Height - _scannerSettings.VerticalCrop))); // height
}
catch (Exception ex)
{
throw new ImageScannerException("Was not able to set scanning parameters.\n" + ex.Message);
}

// if we reached this point, then scanner is probably initialized.
bool isTransferring = true;
foreach (string format in item.Formats)
{
while (isTransferring)
{
try
{
WIA.ImageFile file = (item.Transfer(format)) as WIA.ImageFile;
if (file != null)
{
Stream stream = new MemoryStream();
stream.Write(file.FileData.get_BinaryData() as Byte[], 0, (file.FileData.get_BinaryData() as Byte[]).Length);

// resetting stream position to beginning after data was written into it.
stream.Position = 0;
Bitmap bitmap = new Bitmap(stream);
images.Add(bitmap);
}
else
isTransferring = false; // something happend and we didn't get image
}
catch (Exception ex)
{
// most likely done transferring
// I was not able to find a way to pole scanner for paper feed status.
isTransferring = false;

// scanner's paper feeder was not loaded with paper.
if (images.Count() == 0)
throw new ImageScannerException("Scanner is not loaded with paper or not ready.");

}
}
}

return images;
}

#endregion //Public Methods

#region Private Methods

private void setItem(IItem item, object property, object value)
{
WIA.Property aProperty = item.Properties.get_Item(ref property);
aProperty.set_Value(ref value);
}

private Device GetDevice(Settings settings)
{
Device device=null;
CommonDialogClass dialog = new CommonDialogClass();
if (String.IsNullOrEmpty(settings.DeviceId))
{
device = dialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true, false);
if (device != null)
{
settings.DeviceId = device.DeviceID;
settings.Save();
}
}
return device;
}

private Device GetDevice(string deviceId)
{
WIA.DeviceManager manager = new DeviceManager();
Device device=null;
foreach( DeviceInfo info in manager.DeviceInfos)
{
if(info.DeviceID == deviceId)
{
device = info.Connect();
break;
}
}
return device;
}
#endregion //Private Methods
}
}

ImageScannerException:

using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace Bmec.ScanLibrary.Scan
{
public class ImageScannerException : Exception, ISerializable
{
#region Properties

public string UserFriendlyMessage { get; private set; }

#endregion //Properties

#region Constructors

/// <summary>
/// All messages are set to String.Empty.
/// </summary>
public ImageScannerException()
: base(String.Empty)
{
UserFriendlyMessage = String.Empty;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error message.</param>
public ImageScannerException(string message)
: base(message)
{
UserFriendlyMessage = message;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error Message</param>
/// <param name="innerException">Inner Exception if any, null otherwise. </param>
public ImageScannerException(string message, Exception innerException)
: base(message, innerException)
{
UserFriendlyMessage = message;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public ImageScannerException(string message, string userFriendlyMessage)
: base(message)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="innerException">Inner Exception if any, null otherwise.
/// (If inner exception is null use different overloaded constructor)</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public ImageScannerException(string message, Exception innerException, string userFriendlyMessage)
: base(message, innerException)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
public ImageScannerException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
UserFriendlyMessage = info.GetString("UserFriendlyMessage");
}

#endregion //Constructors

#region Methods

/// <summary>
/// Implementation for ISerializable.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("UserFriendlyMessage", UserFriendlyMessage);
}

#endregion //Methods
}
}

Now to be able to easily use these classes I created a helper class which combines scanning functionality with PDF conversion:

ScannerHelper:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bmec.ScanLibrary.Converters;
using System.IO;

namespace Bmec.ScanLibrary.Scan
{
public class ScannerHelper
{
public ScannerSettings ScannerSettings { get; set; }
public PdfSettings PdfSettings { get; set; }

/// <summary>
/// Constructor with default settings.
/// </summary>
public ScannerHelper()
{
ScannerSettings = new ScannerSettings(ScanningSize.Letter,120, Color.BlackWhite);
PdfSettings = new PdfSettings(PdfOrientation.Portrait, PdfSize.Letter);
}

/// <summary>
/// Customizable constructor
/// </summary>
/// <param name="scannerSettings">Settings for the scanner.</param>
/// <param name="pdfSettings">Settings for PDF page(s).</param>
public ScannerHelper(ScannerSettings scannerSettings, PdfSettings pdfSettings)
{
ScannerSettings = scannerSettings;
PdfSettings = pdfSettings;
}

public void ScanToPdf(string newPdfFileName)
{
ImageScanner scanner = new ImageScanner(ScannerSettings);
PdfConverter converter = new PdfConverter(PdfSettings);

converter.SaveFrom(scanner.Scan(), newPdfFileName);
}

/// <summary>
/// This method scans to Pdf Stream.
/// </summary>
/// <returns>Return open stream with PDF binary data in it. The stream needs to be flushed and closed after use.</returns>
public Stream ScanToPdf()
{
ImageScanner scanner = new ImageScanner(ScannerSettings);
PdfConverter converter = new PdfConverter(PdfSettings);

return converter.ConvertFrom(scanner.Scan());
}
}
}

Here are couple of overloads. One lets you scan into a file, the other one returns stream of PDF data, which you may use in your program.

That’s it for scanning.

8 comments:

  1. Christoph LechleitnerFebruary 12, 2012 at 5:24 AM

    Thanks a lot for this post!

    Learning about these undocumented settings with property IDs 6147-6152 allowed us to prevent a customer from major embarrassment ;-))

    Regards Christoph from Innsbruck, Austria

    ReplyDelete
  2. How can i download this sample application......

    ReplyDelete
  3. Chandra, this application is not available for download. I have provided enough source code to implement scanning functionality. I generally do not give complete solutions, you've got to put dirt under your nails to have understanding.

    ReplyDelete
  4. Hi Ivan

    In this settings class is missing(I am getting error are you missing a reference or namespace 'settings' library)

    Please help me.......

    ReplyDelete
  5. Chandra,
    The sole purpose of Settings class is to store DeviceId.
    You may store in app.config and retrieve via ConfigurationManager, you may add setting in project properties or you may get rid of settings all together.
    Saving DeviceId prevents Scanner initialization window to show up all the time.

    Thanks!

    ReplyDelete
  6. Hi ivan,

    How to convert pdf to bmp images in c#....



    Any ideas please helpme

    ReplyDelete
  7. Hi ivan,

    whether this code is helpful for duplex scanners(two-side scanning of images)

    ReplyDelete
  8. Hari,

    It should be helpful to some extent, but I have not tried it.

    ReplyDelete