What I would like to do is to pass an arbitrary audio file to a DirectShow filtergraph and receive a (PCM audio) stream object in the end using .NET 3.5 C# and DirectShow.NET. I would like to reach the point that I can just say:
Stream OpenFile(string filename) {...}
and
stream.Read(...)
I have been reading up on DirectShow for a couple of days and think I have started to grasp the idea of filters and filtergraphs. I found examples (to file / to device) how to play audio or write it to a file, but cannot seem to find the solution for a Stream object. Is this even possible? Could you point me in the right direction in case I missed something, please?
Best,
Hauke
I would like to share my solution to my own problem with you (my focus was on the exotic bwf file format. hence the name.):
using System;
using System.Collections.Generic;
using System.Text;
using DirectShowLib;
using System.Runtime.InteropServices;
using System.IO;
namespace ConvertBWF2WAV
{
public class BWF2WavConverter : ISampleGrabberCB
{
IFilterGraph2 gb = null;
ICaptureGraphBuilder2 icgb = null;
IBaseFilter ibfSrcFile = null;
DsROTEntry m_rot = null;
IMediaControl m_mediaCtrl = null;
ISampleGrabber sg = null;
public BWF2WavConverter()
{
// Initialize
int hr;
icgb = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();
gb = (IFilterGraph2) new FilterGraph();
sg = (ISampleGrabber)new SampleGrabber();
#if DEBUG
m_rot = new DsROTEntry(gb);
#endif
hr = icgb.SetFiltergraph(gb);
DsError.ThrowExceptionForHR(hr);
}
public void reset()
{
gb = null;
icgb = null;
ibfSrcFile = null;
m_rot = null;
m_mediaCtrl = null;
}
public void convert(object obj)
{
string[] pair = obj as string[];
string srcfile = pair[0];
string targetfile = pair[1];
int hr;
ibfSrcFile = (IBaseFilter)new AsyncReader();
hr = gb.AddFilter(ibfSrcFile, "Reader");
DsError.ThrowExceptionForHR(hr);
IFileSourceFilter ifileSource = (IFileSourceFilter)ibfSrcFile;
hr = ifileSource.Load(srcfile, null);
DsError.ThrowExceptionForHR(hr);
// the guid is the one from ffdshow
Type fftype = Type.GetTypeFromCLSID(new Guid("0F40E1E5-4F79-4988-B1A9-CC98794E6B55"));
object ffdshow = Activator.CreateInstance(fftype);
hr = gb.AddFilter((IBaseFilter)ffdshow, "ffdshow");
DsError.ThrowExceptionForHR(hr);
// the guid is the one from the WAV Dest sample in the SDK
Type type = Type.GetTypeFromCLSID(new Guid("3C78B8E2-6C4D-11d1-ADE2-0000F8754B99"));
object wavedest = Activator.CreateInstance(type);
hr = gb.AddFilter((IBaseFilter)wavedest, "WAV Dest");
DsError.ThrowExceptionForHR(hr);
// manually tell the graph builder to try to hook up the pin that is left
IPin pWaveDestOut = null;
hr = icgb.FindPin(wavedest, PinDirection.Output, null, null, true, 0, out pWaveDestOut);
DsError.ThrowExceptionForHR(hr);
// render step 1
hr = icgb.RenderStream(null, null, ibfSrcFile, (IBaseFilter)ffdshow, (IBaseFilter)wavedest);
DsError.ThrowExceptionForHR(hr);
// Configure the sample grabber
IBaseFilter baseGrabFlt = sg as IBaseFilter;
ConfigSampleGrabber(sg);
IPin pGrabberIn = DsFindPin.ByDirection(baseGrabFlt, PinDirection.Input, 0);
IPin pGrabberOut = DsFindPin.ByDirection(baseGrabFlt, PinDirection.Output, 0);
hr = gb.AddFilter((IBaseFilter)sg, "SampleGrabber");
DsError.ThrowExceptionForHR(hr);
AMMediaType mediatype = new AMMediaType();
sg.GetConnectedMediaType(mediatype);
hr = gb.Connect(pWaveDestOut, pGrabberIn);
DsError.ThrowExceptionForHR(hr);
// file writer
FileWriter file_writer = new FileWriter();
IFileSinkFilter fs = (IFileSinkFilter)file_writer;
fs.SetFileName(targetfile, null);
hr = gb.AddFilter((DirectShowLib.IBaseFilter)file_writer, "File Writer");
DsError.ThrowExceptionForHR(hr);
// render step 2
AMMediaType mediatype2 = new AMMediaType();
pWaveDestOut.ConnectionMediaType(mediatype2);
gb.Render(pGrabberOut);
// alternatively to the file writer use the NullRenderer() to just discard the rest
// assign control
m_mediaCtrl = gb as IMediaControl;
// run
hr = m_mediaCtrl.Run();
DsError.ThrowExceptionForHR(hr);
}
//
// configure the SampleGrabber filter of the graph
//
void ConfigSampleGrabber(ISampleGrabber sampGrabber)
{
AMMediaType media;
// set the media type. works with "stream" somehow...
media = new AMMediaType();
media.majorType = MediaType.Stream;
//media.subType = MediaSubType.WAVE;
//media.formatType = FormatType.WaveEx;
// that's the call to the ISampleGrabber interface
sg.SetMediaType(media);
DsUtils.FreeAMMediaType(media);
media = null;
// set BufferCB as the desired Callback function
sg.SetCallback(this, 1);
}
public int SampleCB(double a, IMediaSample b)
{
return 0;
}
/// <summary>
/// Called on each SampleGrabber hit.
/// </summary>
/// <param name="SampleTime">Starting time of the sample, in seconds.</param>
/// <param name="pBuffer">Pointer to a buffer that contains the sample data.</param>
/// <param name="BufferLen">Length of the buffer pointed to by pBuffer, in bytes.</param>
/// <returns></returns>
public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
byte[] buffer = new byte[BufferLen];
Marshal.Copy(pBuffer, buffer, 0, BufferLen);
using (BinaryWriter binWriter = new BinaryWriter(File.Open(#"C:\directshowoutput.pcm", FileMode.Append)))
{
binWriter.Write(buffer);
}
return 0;
}
}
}
This (AVILibrary Wrapper) may lead you to a solution, it's not DirectSound based (which I get the feeling is very much biased to interfacing your code with the playback hardware) but could be the answer.
Another approach can be found here.
How about NAudio ? http://www.codeplex.com/naudio
It has a stream implementation.
Related
I am building a screen recording app in C# using Windows Graphics Capture API. I am using this script. I can select monitor and can record it to mp4 file. I can also select a Window and record it too. But how can we record a region with this? Ideally I need to give x,y coordinates along with width and height to record that specific region.
Here are the functions which return GraphicsCaptureItem for Window or Monitor hwnd, which can be used to record.
public static GraphicsCaptureItem CreateItemForWindow(IntPtr hwnd)
{
var factory = WindowsRuntimeMarshal.GetActivationFactory(typeof(GraphicsCaptureItem));
var interop = (IGraphicsCaptureItemInterop)factory;
var temp = typeof(GraphicsCaptureItem);
var itemPointer = interop.CreateForWindow(hwnd, GraphicsCaptureItemGuid);
var item = Marshal.GetObjectForIUnknown(itemPointer) as GraphicsCaptureItem;
Marshal.Release(itemPointer);
return item;
}
public static GraphicsCaptureItem CreateItemForMonitor(IntPtr hmon)
{
var factory = WindowsRuntimeMarshal.GetActivationFactory(typeof(GraphicsCaptureItem));
var interop = (IGraphicsCaptureItemInterop)factory;
var temp = typeof(GraphicsCaptureItem);
var itemPointer = interop.CreateForMonitor(hmon, GraphicsCaptureItemGuid);
var item = Marshal.GetObjectForIUnknown(itemPointer) as GraphicsCaptureItem;
Marshal.Release(itemPointer);
return item;
}
And this is Recording function
private async void RecordScreen(GraphicsCaptureItem item)
{
_device = Direct3D11Helpers.CreateDevice();
// Get our encoder properties
uint frameRate = 30;
uint bitrate = 3 * 1000000;
var width = (uint)item.Size.Width;
var height = (uint)item.Size.Height;
// Kick off the encoding
try
{
newFile = GetTempFile();
using (var stream = new FileStream(newFile, FileMode.CreateNew).AsRandomAccessStream())
using (_encoder = new Encoder(_device, item))
{
await _encoder.EncodeAsync(
stream,
width, height, bitrate,
frameRate);
}
}
catch (Exception ex)
{}
}
I achieved this by passing a custom region to CopySubresourceRegion in WaitForNewFrame method.
public SurfaceWithInfo WaitForNewFrame()
{
.....
using (var multithreadLock = new MultithreadLock(_multithread))
using (var sourceTexture = Direct3D11Helpers.CreateSharpDXTexture2D(_currentFrame.Surface))
{
.....
using (var copyTexture = new SharpDX.Direct3D11.Texture2D(_d3dDevice, description))
{
.....
var region = new SharpDX.Direct3D11.ResourceRegion(
_region.Left,
_region.Top,
0,
_region.Left + _region.Width,
_region.Top + _region.Height,
1
);
_d3dDevice.ImmediateContext.CopyResource(_blankTexture, copyTexture);
_d3dDevice.ImmediateContext.CopySubresourceRegion(sourceTexture, 0, region, copyTexture, 0);
result.Surface = Direct3D11Helpers.CreateDirect3DSurfaceFromSharpDXTexture(copyTexture);
}
}
....
}
https://learn.microsoft.com/en-us/windows/desktop/api/devicetopology/nf-devicetopology-iaudioautogaincontrol-setenabled
I'm trying to do a C# wrapper/call to set this low level device setting value. It basically disables/enables AGC microphone setting. I've found this link but i'm not sure how to wire it up to look like this:
// http://msdn.microsoft.com/en-us/library/dd757304%28VS.85%29.aspx
[DllImport("winmm.dll", CharSet = CharSet.Ansi)]
public static extern Int32 mixerGetNumDevs();
Essentially, I want to disable (uncheck) this enhancement
This answer has discussed "How to use interface pointer exported by C++ DLL in C#". But, I think what you want more is as below.
You don't need to use the winapi. The auto gain control functionality is implemented in the AudioQualityEnhancer class, which is a mediahandler. It uses the AutoGainControl bool property to enable or disable the gain control feature.
Automatic Gain Control example in C#:
using System;
using Ozeki.Media;
namespace Automatic_Gain_Control
{
class Program
{
static Microphone microphone;
static Speaker speaker;
static MediaConnector connector;
static AudioQualityEnhancer audioProcessor;
static void Main(string[] args)
{
microphone = Microphone.GetDefaultDevice();
speaker = Speaker.GetDefaultDevice();
connector = new MediaConnector();
audioProcessor = new AudioQualityEnhancer();
audioProcessor.AutoGainControl = true;//enable
audioProcessor.GainSpeed = 12;
audioProcessor.MaxGain = 30;
connector.Connect(microphone, audioProcessor);
connector.Connect(audioProcessor, speaker);
microphone.Start();
speaker.Start();
Console.ReadLine();
}
}
}
UPDATE:
To disable the AGC with interface simply, You can encapsulate all interface procedures in your own DLL functions, like:
HRESULT EnableAGC()
{
CComPtr<IMMDeviceEnumerator> m_pIMMEnumerator;
CComPtr<IAudioVolumeLevel> m_pMicBoost;
CComPtr<IAudioAutoGainControl> m_pAGC;
HRESULT hr = S_OK;
hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator, (void**)&m_pIMMEnumerator);
if (FAILED(hr)) return hr;
CComPtr<IMMDevice> pIMMDeivce = NULL;
std::wstring strEndPointID;//String of the Device ID
if (strEndPointID.empty())
{
hr = m_pIMMEnumerator->GetDefaultAudioEndpoint(eCapture, eConsole, &pIMMDeivce);
}
else
{
hr = m_pIMMEnumerator->GetDevice(strEndPointID.c_str(), &pIMMDeivce);
}
if (FAILED(hr)) return hr;
CComPtr<IDeviceTopology> pTopo = NULL;
hr = pIMMDeivce->Activate(IID_IDeviceTopology, CLSCTX_INPROC_SERVER, 0, (void**)&pTopo);
if (FAILED(hr)) return hr;
CComPtr<IConnector> pConn = NULL;
hr = pTopo->GetConnector(0, &pConn);
if (FAILED(hr)) return hr;
CComPtr<IConnector> pConnNext = NULL;
hr = pConn->GetConnectedTo(&pConnNext);
if (FAILED(hr)) return hr;
CComPtr<IPart> pPart = NULL;
hr = pConnNext->QueryInterface(IID_IPart, (void**)&pPart);
if (FAILED(hr)) return hr;
hr = pPart->Activate(CLSCTX_INPROC_SERVER, IID_IAudioAutoGainControl, (void**)&m_pAGC);
if (SUCCEEDED(hr) && m_pAGC)
{
//Hardware Supports Microphone AGC
BOOL bEnable = TRUE;
hr = m_pAGC->SetEnabled(bEnable, NULL);
}
else
{
//Hardware not Supports Microphone AGC
}
return hr;
}
Here is a pure C# console app sample, equivalent of #Drake's C/C++ code. I've written it using code from an open source project called DirectN that defines thousands of c# Windows interop types (DirectX, etc.), including Code Audio API, etc.
class Program
{
static void Main(string[] args)
{
// using DirectN
var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
// or call GetDevice(...) with an id
enumerator.GetDefaultAudioEndpoint(
__MIDL___MIDL_itf_mmdeviceapi_0000_0000_0001.eCapture,
__MIDL___MIDL_itf_mmdeviceapi_0000_0000_0002.eConsole,
out var device).ThrowOnError();
const int CLSCTX_ALL = 23;
device.Activate(typeof(IDeviceTopology).GUID, CLSCTX_ALL, null, out var iface).ThrowOnError();
var topology = (IDeviceTopology)iface;
topology.GetConnector(0, out var connector).ThrowOnError();
var part = (IPart)connector;
if (part.Activate(CLSCTX_ALL, typeof(IAudioAutoGainControl).GUID, out iface).IsError)
{
Console.WriteLine("AGC not supported.");
return;
}
var control = (IAudioAutoGainControl)iface;
control.SetEnabled(true, IntPtr.Zero);
}
[ComImport]
[Guid("bcde0395-e52f-467c-8e3d-c4579291692e")] // CLSID_MMDeviceEnumerator
class MMDeviceEnumerator
{
}
}
You can use either the DirectN's nuget package, or copy to your project only the needed .cs files (and their dependencies). Here, you would need the following:
HRESULT.cs
HRESULTS.cs
IAudioAutoGainControl.cs
IAudioVolumeLevel.cs
IConnector.cs
IControlChangeNotify.cs
IControlInterface.cs
IDeviceTopology.cs
IMMDevice.cs
IMMDeviceCollection.cs
IMMDeviceEnumerator.cs
IMMNotificationClient.cs
IPart.cs
IPartsList.cs
IPerChannelDbLevel.cs
ISubunit.cs
PROPERTYKEY.cs
PropertyType.cs
PropVariant.cs
_tagpropertykey.cs
__MIDL___MIDL_itf_devicetopology_0000_0000_0011.cs
__MIDL___MIDL_itf_devicetopology_0000_0000_0012.cs
__MIDL___MIDL_itf_devicetopology_0000_0000_0013.cs
__MIDL___MIDL_itf_mmdeviceapi_0000_0000_0001.cs
__MIDL___MIDL_itf_mmdeviceapi_0000_0000_0002.cs
And when I quit the program the mp4 file deleted automatic.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using DirectShowLib;
using DirectShowLib.BDA;
using DirectShowLib.DES;
using DirectShowLib.DMO;
using DirectShowLib.Dvd;
using DirectShowLib.MultimediaStreaming;
using DirectShowLib.SBE;
using System.Runtime.InteropServices;
using System.Management;
using System.IO;
using AForge.Video;
using AForge.Video.DirectShow;
using AForge.Video.FFMPEG;
using AForge.Video.VFW;
using System.Drawing.Imaging;
namespace Youtube_Manager
{
public partial class Elgato_Video_Capture : Form
{
IFileSinkFilter sink;
IFilterGraph2 graph;
ICaptureGraphBuilder2 captureGraph;
Size videoSize;
string error = "";
List<Object> devices = new List<Object>();
IMediaControl mediaControl;
public Elgato_Video_Capture()
{
InitializeComponent();
if (comboBox1.Items.Count == 0)
{
for (int xx = 1; xx <= 8; xx++)
{
comboBox1.Items.Add(xx);
}
}
InitDevice();
}
IPin outPin;
IPin inPin;
private void InitDevice()
{
try
{
//Set the video size to use for capture and recording
videoSize = new Size(827, 505);//1280, 720);
//Initialize filter graph and capture graph
graph = (IFilterGraph2)new FilterGraph();
captureGraph = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();
captureGraph.SetFiltergraph(graph);
//Create filter for Elgato
Guid elgatoGuid = new Guid("39F50F4C-99E1-464A-B6F9-D605B4FB5918");
Type comType = Type.GetTypeFromCLSID(elgatoGuid);
IBaseFilter elgatoFilter = (IBaseFilter)Activator.CreateInstance(comType);
graph.AddFilter(elgatoFilter, "Elgato Video Capture Filter");
//Create smart tee filter, add to graph, connect Elgato's video out to smart tee in
IBaseFilter smartTeeFilter = (IBaseFilter)new SmartTee();
graph.AddFilter(smartTeeFilter, "Smart Tee");
outPin = GetPin(elgatoFilter, "Video");
inPin = GetPin(smartTeeFilter, "Input");
graph.Connect(outPin, inPin);
//Create video renderer filter, add it to graph, connect smartTee Preview pin to video renderer's input pin
IBaseFilter videoRendererFilter = (IBaseFilter)new VideoRenderer();
graph.AddFilter(videoRendererFilter, "Video Renderer");
outPin = GetPin(smartTeeFilter, "Capture");
inPin = GetPin(videoRendererFilter, "Input");
graph.Connect(outPin, inPin);
captureGraph.SetOutputFileName(MediaSubType.Avi, #"e:\screenshots\test1.mp4", out smartTeeFilter, out sink);
sink.SetFileName(#"e:\screenshots\test1.mp4", null);
//Render stream from video renderer
captureGraph.RenderStream(PinCategory.Capture, MediaType.Video, videoRendererFilter, null, null);
//Set the video preview to be the videoFeed panel
IVideoWindow vw = (IVideoWindow)graph;
vw.put_Owner(pictureBox1.Handle);
vw.put_MessageDrain(this.Handle);
vw.put_WindowStyle(WindowStyle.Child | WindowStyle.ClipSiblings | WindowStyle.ClipChildren);
vw.SetWindowPosition(0, 0, 827, 505);
//Start the preview
mediaControl = graph as IMediaControl;
mediaControl.Run();
}
catch (Exception err)
{
error = err.ToString();
}
}
IPin GetPin(IBaseFilter filter, string pinname)
{
IEnumPins epins;
int hr = filter.EnumPins(out epins);
checkHR(hr, "Can't enumerate pins");
IntPtr fetched = Marshal.AllocCoTaskMem(4);
IPin[] pins = new IPin[1];
while (epins.Next(1, pins, fetched) == 0)
{
PinInfo pinfo;
pins[0].QueryPinInfo(out pinfo);
bool found = (pinfo.name == pinname);
DsUtils.FreePinInfo(pinfo);
if (found)
return pins[0];
}
checkHR(-1, "Pin not found");
return null;
}
public void checkHR(int hr, string msg)
{
if (hr < 0)
{
MessageBox.Show(msg);
DsError.ThrowExceptionForHR(hr);
}
}
}
I added this part now to my code:
captureGraph.SetOutputFileName(MediaSubType.Avi, #"e:\screenshots\test1.mp4", out smartTeeFilter, out sink);
sink.SetFileName(#"e:\screenshots\test1.mp4", null);
I see the preview but the mp4 file is empty it's not saving the video to the file at all.
What I wanted to do is to save the video stream using the directshow to a mp4 file.
Now all I can get is to see a preview of the video in a pictureBox.
You don't capture to a MP4-File, you capture to an AVI-File with just an MP4-extension! DirectShow has no native MP4-Mux. You need to install one seperatly like the GDCL MP4 Mux.
I don't think your connections are working correctly. With DirectShowNet you need to check the return codes and then throw the exceptions yourself, like:
int hr = graph.Connect(outPin, inPin);;
DsError.ThrowExceptionForHR(hr);
Your captureGraph.RenderStream is useless because a VideoRender-Filter has no output-pin. Please look at the meaning of this function. You can better build this graph with just RenderStream, it is even inserting the SmartTee-Filter for you.
I am using a C# FreeImage wrapper.
I am trying to open a PDF file containing images, and "extract" those images into windows Bitmap objects. I am following the guidelines described in articles on the internet, following loosely the following pattern:
byte[] bytes = GetPdfFile();
iTextSharp.text.pdf.PdfReader reader = new iTextSharp.text.pdf.PdfReader(bytes);
int pageNumber = 0;
for (int i = 0; i <= reader.XrefSize - 1; i++)
{
pdfObj = reader.GetPdfObject(i);
if ((pdfObj != null) && pdfObj.IsStream())
{
pdfStream = (iTextSharp.text.pdf.PdfStream)pdfObj;
iTextSharp.text.pdf.PdfObject subtype = pdfStream.Get(iTextSharp.text.pdf.PdfName.SUBTYPE);
if ((subtype != null) && subtype.ToString().Equals(iTextSharp.text.pdf.PdfName.IMAGE.ToString()))
{
pageNumber++;
byte[] imgBytes = iTextSharp.text.pdf.PdfReader.GetStreamBytesRaw((iTextSharp.text.pdf.PRStream)pdfStream);
if ((imgBytes != null))
{
// in my case images are in TIF Group 4 format
using (MemoryStream ms = new MemoryStream(imgBytes))
{
FreeImageAPI.FIBITMAP bmp = FreeImageAPI.FreeImage.LoadFromStream(ms);
// in my case bmp with IsNull = true is returned
if (!bmp.IsNull)
{
using (MemoryStream msOut = new MemoryStream())
{
FreeImageAPI.FreeImage.SaveToStream(bmp, msOut, ... ); // etc.
}
}
}
}
}
}
}
Does anybody have a suggestion on how to troubleshoot this, given that no exception is returned - some kind of GetLastError FreeImage function?
Thank you
Although it doesn't appear that the SetOutputMessage function was added to the .NET wrapper library, it is possible to call it directly from the FreeImage library.
That is, adding the function to your code using:
[DllImport("FreeImage", EntryPoint = "FreeImage_SetOutputMessage")]
internal static extern void SetOutputMessage(OutputMessageFunction outputMessageFunction);
The following code shows an Exception being thrown from the Save function, rather than returning false (with the following console output):
Test 1... Success
Test 2...
Unhandled Exception: System.Exception: only 24-bit highcolor or 8-bit greyscale/palette bitmaps can be saved as JPEG
at ConsoleApplication1.Program.OutputMessage(FREE_IMAGE_FORMAT format, String message) ...\ConsoleApplication1\Program.cs:line 35
at FreeImageAPI.FreeImage.Save(FREE_IMAGE_FORMAT fif, FIBITMAP dib, String filename, FREE_IMAGE_SAVE_FLAGS flags)
at ConsoleApplication1.Program.Main(String[] args) in ...\ConsoleApplication1\Program.cs:line 28
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using FreeImageAPI;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{ FIBITMAP bitmap;
bool success;
SetOutputMessage(OutputMessage);
Console.Write("Test 1... ");
bitmap = FreeImage.CreateFromBitmap(new Bitmap(320, 240, PixelFormat.Format24bppRgb));
success = FreeImage.Save(FREE_IMAGE_FORMAT.FIF_JPEG, bitmap, "output.jpg", FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYNORMAL);
FreeImage.Unload(bitmap);
Console.WriteLine("Success");
Console.Write("Test 2... ");
bitmap = FreeImage.CreateFromBitmap(new Bitmap(320, 240));
success = FreeImage.Save(FREE_IMAGE_FORMAT.FIF_JPEG, bitmap, "output.jpg", FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYNORMAL);
FreeImage.Unload(bitmap);
Console.WriteLine("Success");
}
static void OutputMessage(FREE_IMAGE_FORMAT format, string message)
{ throw new Exception(message);
}
[DllImport("FreeImage", EntryPoint = "FreeImage_SetOutputMessage")]
internal static extern void SetOutputMessage(OutputMessageFunction outputMessageFunction);
}
}
I'm building a .Net 4.0 application for remote control of a scanner device. I have tried both TWAIN and WIA libraries, but I have the same problem. Scanning images without scanner selection and scanning settings dialogs.
I found a useful article on WIA scripting in .Net, and modified it to this:
private Image Scan(string deviceName)
{
WiaClass wiaManager = null; // WIA manager COM object
CollectionClass wiaDevs = null; // WIA devices collection COM object
ItemClass wiaRoot = null; // WIA root device COM object
CollectionClass wiaPics = null; // WIA collection COM object
ItemClass wiaItem = null; // WIA image COM object
try
{
// create COM instance of WIA manager
wiaManager = new WiaClass();
// call Wia.Devices to get all devices
wiaDevs = wiaManager.Devices as CollectionClass;
if ((wiaDevs == null) || (wiaDevs.Count == 0))
{
throw new Exception("No WIA devices found!");
}
object device = null;
foreach (IWiaDeviceInfo currentDevice in wiaManager.Devices)
{
if (currentDevice.Name == deviceName)
{
device = currentDevice;
break;
}
}
if (device == null)
{
throw new Exception
(
"Device with name \"" +
deviceName +
"\" could not be found."
);
}
// select device
wiaRoot = (ItemClass)wiaManager.Create(ref device);
// something went wrong
if (wiaRoot == null)
{
throw new Exception
(
"Could not initialize device \"" +
deviceName + "\"."
);
}
wiaPics = wiaRoot.GetItemsFromUI
(
WiaFlag.SingleImage,
WiaIntent.ImageTypeColor
) as CollectionClass;
if (wiaPics == null || wiaPics.Count == 0)
{
throw new Exception("Could not scan image.");
}
Image image = null;
// enumerate all the pictures the user selected
foreach (object wiaObj in wiaPics)
{
if (image == null)
{
wiaItem = (ItemClass)Marshal.CreateWrapperOfType
(
wiaObj, typeof(ItemClass)
);
// create temporary file for image
string tempFile = Path.GetTempFileName();
// transfer picture to our temporary file
wiaItem.Transfer(tempFile, false);
// create Image instance from file
image = Image.FromFile(tempFile);
}
// release enumerated COM object
Marshal.ReleaseComObject(wiaObj);
}
if (image == null)
{
throw new Exception("Error reading scanned image.");
}
return image;
}
finally
{
// release WIA image COM object
if (wiaItem != null)
Marshal.ReleaseComObject(wiaItem);
// release WIA collection COM object
if (wiaPics != null)
Marshal.ReleaseComObject(wiaPics);
// release WIA root device COM object
if (wiaRoot != null)
Marshal.ReleaseComObject(wiaRoot);
// release WIA devices collection COM object
if (wiaDevs != null)
Marshal.ReleaseComObject(wiaDevs);
// release WIA manager COM object
if (wiaManager != null)
Marshal.ReleaseComObject(wiaManager);
}
}
With this I actually managed to select the device from configuration (input parameter of the Scan method) and retrieve the resulting image after scan.
But the problem with scanning options dialog (Scan using DEVICENAME). As this is a remote control application, dialog will not be visible to the user, so I need to either skip it using default settings, or use settings from a configuration if necessary.
Scanning options dialog:
In the end I did not use the code written in the question for scanning dialogs. I found a useful example of Scanning with Windows Image Acquisition 2.0 which by the way also had a blocking dialog, but this was easily modified and in moments I had a simple class with a Scan(string scannerId) function which would just scan with a selected device and nothing more, see code () below:
using System;
using System.Collections.Generic;
using System.IO;
using System.Drawing;
namespace WIATest
{
class WIAScanner
{
const string wiaFormatBMP = "{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}";
class WIA_DPS_DOCUMENT_HANDLING_SELECT
{
public const uint FEEDER = 0x00000001;
public const uint FLATBED = 0x00000002;
}
class WIA_DPS_DOCUMENT_HANDLING_STATUS
{
public const uint FEED_READY = 0x00000001;
}
class WIA_PROPERTIES
{
public const uint WIA_RESERVED_FOR_NEW_PROPS = 1024;
public const uint WIA_DIP_FIRST = 2;
public const uint WIA_DPA_FIRST = WIA_DIP_FIRST + WIA_RESERVED_FOR_NEW_PROPS;
public const uint WIA_DPC_FIRST = WIA_DPA_FIRST + WIA_RESERVED_FOR_NEW_PROPS;
//
// Scanner only device properties (DPS)
//
public const uint WIA_DPS_FIRST = WIA_DPC_FIRST + WIA_RESERVED_FOR_NEW_PROPS;
public const uint WIA_DPS_DOCUMENT_HANDLING_STATUS = WIA_DPS_FIRST + 13;
public const uint WIA_DPS_DOCUMENT_HANDLING_SELECT = WIA_DPS_FIRST + 14;
}
/// <summary>
/// Use scanner to scan an image (with user selecting the scanner from a dialog).
/// </summary>
/// <returns>Scanned images.</returns>
public static List<Image> Scan()
{
WIA.ICommonDialog dialog = new WIA.CommonDialog();
WIA.Device device = dialog.ShowSelectDevice(WIA.WiaDeviceType.UnspecifiedDeviceType, true, false);
if (device != null)
{
return Scan(device.DeviceID);
}
else
{
throw new Exception("You must select a device for scanning.");
}
}
/// <summary>
/// Use scanner to scan an image (scanner is selected by its unique id).
/// </summary>
/// <param name="scannerName"></param>
/// <returns>Scanned images.</returns>
public static List<Image> Scan(string scannerId)
{
List<Image> images = new List<Image>();
bool hasMorePages = true;
while (hasMorePages)
{
// select the correct scanner using the provided scannerId parameter
WIA.DeviceManager manager = new WIA.DeviceManager();
WIA.Device device = null;
foreach (WIA.DeviceInfo info in manager.DeviceInfos)
{
if (info.DeviceID == scannerId)
{
// connect to scanner
device = info.Connect();
break;
}
}
// device was not found
if (device == null)
{
// enumerate available devices
string availableDevices = "";
foreach (WIA.DeviceInfo info in manager.DeviceInfos)
{
availableDevices += info.DeviceID + "n";
}
// show error with available devices
throw new Exception("The device with provided ID could not be found. Available Devices:n" + availableDevices);
}
WIA.Item item = device.Items[1] as WIA.Item;
try
{
// scan image
WIA.ICommonDialog wiaCommonDialog = new WIA.CommonDialog();
WIA.ImageFile image = (WIA.ImageFile)wiaCommonDialog.ShowTransfer(item, wiaFormatBMP, false);
// save to temp file
string fileName = Path.GetTempFileName();
File.Delete(fileName);
image.SaveFile(fileName);
image = null;
// add file to output list
images.Add(Image.FromFile(fileName));
}
catch (Exception exc)
{
throw exc;
}
finally
{
item = null;
//determine if there are any more pages waiting
WIA.Property documentHandlingSelect = null;
WIA.Property documentHandlingStatus = null;
foreach (WIA.Property prop in device.Properties)
{
if (prop.PropertyID == WIA_PROPERTIES.WIA_DPS_DOCUMENT_HANDLING_SELECT)
documentHandlingSelect = prop;
if (prop.PropertyID == WIA_PROPERTIES.WIA_DPS_DOCUMENT_HANDLING_STATUS)
documentHandlingStatus = prop;
}
// assume there are no more pages
hasMorePages = false;
// may not exist on flatbed scanner but required for feeder
if (documentHandlingSelect != null)
{
// check for document feeder
if ((Convert.ToUInt32(documentHandlingSelect.get_Value()) &amp;amp; WIA_DPS_DOCUMENT_HANDLING_SELECT.FEEDER) != 0)
{
hasMorePages = ((Convert.ToUInt32(documentHandlingStatus.get_Value()) &amp;amp; WIA_DPS_DOCUMENT_HANDLING_STATUS.FEED_READY) != 0);
}
}
}
}
return images;
}
/// <summary>
/// Gets the list of available WIA devices.
/// </summary>
/// <returns></returns>
public static List<string> GetDevices()
{
List<string> devices = new List<string>();
WIA.DeviceManager manager = new WIA.DeviceManager();
foreach (WIA.DeviceInfo info in manager.DeviceInfos)
{
devices.Add(info.DeviceID);
}
return devices;
}
}
}
First off, many thanks to Miljenko Barbir for his above solution, it works great.
I would like to add that if you want zero dialogs, you can use (from Milijenko's demo code)
WIA.ImageFile image = item.Transfer(wiaFormatBMP);
instead of
WIA.ImageFile image = (WIA.ImageFile)wiaCommonDialog.ShowTransfer(item, wiaFormatBMP, false);
This basically removes the progress bar as well, so you get scanning without any dialogs.
// show scanner view
guif.ShowUI = 0;
guif.ModalUI = 0;
You can see in this code that's I've implemented.