How to Build Desktop .NET Document Scanner Application on Windows and Linux
Tags: dotnet, csharp, document, edgedetection
Published: 2022-09-22 08:38:21

Suppose you are a C# developer who wants to use [Dynamsoft Document Normalizer](https://www.dynamsoft.com/document-normalizer/docs/introduction/?ver=latest) to implement a desktop document scanner application. Since Dynamsoft Document Normalizer is a C/C++ library, you may encounter some interoperability issues when using it in your C# project. In this article, we will help you build a .NET document scanner SDK based on Dynamsoft C/C++ Document Normalizer. With the SDK, you can quickly implement desktop document scanner applications in C# on Windows and Linux.

## NuGet Package
[https://www.nuget.org/packages/DocumentScannerSDK](https://www.nuget.org/packages/DocumentScannerSDK)

## Building .NET Document Scanner SDK with Dynamsoft C/C++ Document Normalizer
In this section, you will see how to package a .NET library with third-party C/C++ `*.dll` and `*.so` files, as well as how to glue C# and C/C++ code.

### Create a .NET Library Project with Native Dependencies
Create a new C# library project named `DocumentScannerSDK` via dotnet CLI:

```bash
dotnet new classlib -o DocumentScannerSDK
```

Open `DocumentScannerSDK.csproj` to configure the runtime libraries that the SDK depends on:

```xml
<ItemGroup>
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-file-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-file-l1-2-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-file-l2-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-file-l2-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-localization-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-localization-l1-2-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-processthreads-l1-1-1.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-processthreads-l1-1-1.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-synch-l1-2-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-synch-l1-2-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-core-timezone-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-core-timezone-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-conio-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-conio-l1-1-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-convert-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-convert-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-environment-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-environment-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-filesystem-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-filesystem-l1-1-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-heap-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-heap-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-locale-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-locale-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-math-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-math-l1-1-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-multibyte-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-multibyte-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-runtime-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-runtime-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-stdio-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-stdio-l1-1-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-string-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-string-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-time-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-time-l1-1-0.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/api-ms-win-crt-utility-l1-1-0.dll" Pack="true" PackagePath="runtimes/win-x64/native/api-ms-win-crt-utility-l1-1-0.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/concrt140.dll" Pack="true" PackagePath="runtimes/win-x64/native/concrt140.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftCorex64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftCorex64.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftDocumentNormalizerx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftDocumentNormalizerx64.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftImageProcessingx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftImageProcessingx64.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/DynamsoftIntermediateResultx64.dll" Pack="true" PackagePath="runtimes/win-x64/native/DynamsoftIntermediateResultx64.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140_1.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140_1.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/msvcp140_2.dll" Pack="true" PackagePath="runtimes/win-x64/native/msvcp140_2.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/ucrtbase.dll" Pack="true" PackagePath="runtimes/win-x64/native/ucrtbase.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vccorlib140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vccorlib140.dll"  />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vcomp140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vcomp140.dll" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/win/vcruntime140.dll" Pack="true" PackagePath="runtimes/win-x64/native/vcruntime140.dll" />

    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftCore.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftCore.so" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftDocumentNormalizer.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftDocumentNormalizer.so" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftImageProcessing.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftImageProcessing.so" />
    <None CopyToOutputDirectory="Always" Link="\%(Filename)%(Extension)" Include="platform/linux/libDynamsoftIntermediateResult.so" Pack="true" PackagePath="runtimes/linux-x64/native/libDynamsoftIntermediateResult.so" />
  </ItemGroup>
```

We put `*.dll` and `*.so` file respectively to `runtimes/win-x64/native` and `runtimes/linux-x64/native` folder.

The quantity of `*.dll` files looks overwhelming. Don't panic. It will be reduced in next version.

### Import Unmanaged Native Functions with DLLImport in C# 

Let's take a glimpse at the header file of Dynamsoft C/C++ Document Normalizer:

```cpp
#ifdef __cplusplus
extern "C" {
#endif

	DDN_API const char* DDN_GetVersion();

	DDN_API void* DDN_CreateInstance();

	DDN_API void DDN_DestroyInstance(void* normalizer);

	DDN_API int DDN_InitRuntimeSettingsFromString(void* normalizer, const char* content, char errorMsgBuffer[], const int errorMsgBufferLen);

	DDN_API int DDN_DetectQuadFromFile(void* normalizer, const char* sourceFilePath, const char* templateName, DetectedQuadResultArray** result);

	DDN_API int DDN_DetectQuadFromBuffer(void* normalizer, const ImageData* sourceImage, const char* templateName, DetectedQuadResultArray** result);

	DDN_API int DDN_NormalizeFile(void* normalizer, const char* sourceFilePath, const char* templateName, const Quadrilateral *quad, NormalizedImageResult** result);

	DDN_API int DDN_NormalizeBuffer(void* normalizer, const ImageData* sourceImage, const char* templateName, const Quadrilateral *quad, NormalizedImageResult** result);

	DDN_API void DDN_FreeNormalizedImageResult(NormalizedImageResult** result);

	DDN_API void DDN_FreeDetectedQuadResultArray(DetectedQuadResultArray** results);

	DDN_API int DDN_SaveImageDataToFile(const ImageData* imageData, const char * filePath);

#ifdef __cplusplus
}
#endif
```

To call these functions in C#, we need to import them with the attribute `DllImport`. 

```csharp
#if _WINDOWS
    [DllImport("DynamsoftCorex64")]
    static extern int DC_InitLicense(string license, [Out] byte[] errorMsg, int errorMsgSize);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern IntPtr DDN_CreateInstance();

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern void DDN_DestroyInstance(IntPtr handler);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern IntPtr DDN_GetVersion();

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_InitRuntimeSettingsFromString(IntPtr handler, string settings, [Out] byte[] errorMsg, int errorMsgSize);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_DetectQuadFromFile(IntPtr handler, string sourceFilePath, string templateName, ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_FreeDetectedQuadResultArray(ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_NormalizeFile(IntPtr handler, string sourceFilePath, string templateName, IntPtr quad, ref IntPtr result);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_FreeNormalizedImageResult(ref IntPtr result);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_SaveImageDataToFile(IntPtr image, string filename);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_DetectQuadFromBuffer(IntPtr handler, IntPtr sourceImage, string templateName, ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizerx64")]
    static extern int DDN_NormalizeBuffer(IntPtr handler, IntPtr sourceImage, string templateName, IntPtr quad, ref IntPtr result);

#else 
    [DllImport("DynamsoftCore")]
    static extern int DC_InitLicense(string license, [Out] byte[] errorMsg, int errorMsgSize);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern IntPtr DDN_CreateInstance();

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern void DDN_DestroyInstance(IntPtr handler);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern IntPtr DDN_GetVersion();

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_InitRuntimeSettingsFromString(IntPtr handler, string settings, [Out] byte[] errorMsg, int errorMsgSize);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_DetectQuadFromFile(IntPtr handler, string sourceFilePath, string templateName, ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_FreeDetectedQuadResultArray(ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_NormalizeFile(IntPtr handler, string sourceFilePath, string templateName, IntPtr quad, ref IntPtr result);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_FreeNormalizedImageResult(ref IntPtr result);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_SaveImageDataToFile(IntPtr image, string filename);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_DetectQuadFromBuffer(IntPtr handler, IntPtr sourceImage, string templateName, ref IntPtr pResultArray);

    [DllImport("DynamsoftDocumentNormalizer")]
    static extern int DDN_NormalizeBuffer(IntPtr handler, IntPtr sourceImage, string templateName, IntPtr quad, ref IntPtr result);

#endif

```

Why do we use macro definition `_WINDOWS`? Because the name of the shared library file is different on Windows and Linux. To make the macro definition work, we need to define the preprocessor variable in `csproj` file:

```xml
<!-- https://stackoverflow.com/questions/30153797/c-sharp-preprocessor-differentiate-between-operating-systems -->
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
    <DefineConstants>_WINDOWS</DefineConstants>
</PropertyGroup>
```

### Use StructLayout to Marshal C/C++ Structs
Define following structs in C#:

```csharp
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct NormalizedImageResult
{
    public IntPtr ImageData;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct ImageData
{
    public int bytesLength;

    public IntPtr bytes;

    public int width;

    public int height;

    public int stride;

    public ImagePixelFormat format;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DetectedQuadResultArray
{
    public int resultsCount;
    public IntPtr results;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DetectedQuadResult
{
    public IntPtr location;
    public int confidenceAsDocumentBoundary;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct Quadrilateral
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public DM_Point[] points;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct DM_Point
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
    public int[] coordinate;
}
```

As we call the C/C++ functions, we copy unmanaged memory to managed memory by using `Marshal.PtrToStructure`. For example, we get the detected quadrilaterals from the C/C++ function `DDN_DetectQuadFromFile`:

```csharp
public Result[]? DetectFile(string filename)
{
    if (handler == IntPtr.Zero) return null;

    IntPtr pResultArray = IntPtr.Zero;

    int ret = DDN_DetectQuadFromFile(handler, filename, "", ref pResultArray);
    return GetResults(pResultArray);
}

private Result[]? GetResults(IntPtr pResultArray)
{
    Result[]? resultArray = null;
    if (pResultArray != IntPtr.Zero)
    {
        DetectedQuadResultArray? results = (DetectedQuadResultArray?)Marshal.PtrToStructure(pResultArray, typeof(DetectedQuadResultArray));
        if (results != null)
        {
            int count = results.Value.resultsCount;
            if (count > 0)
            {
                IntPtr[] documents = new IntPtr[count];
                Marshal.Copy(results.Value.results, documents, 0, count);
                resultArray = new Result[count];

                for (int i = 0; i < count; i++)
                {
                    DetectedQuadResult? result = (DetectedQuadResult?)Marshal.PtrToStructure(documents[i], typeof(DetectedQuadResult));
                    if (result != null)
                    {
                        Result r = new Result();
                        resultArray[i] = r;
                        r.Confidence = result.Value.confidenceAsDocumentBoundary;
                        Quadrilateral? Quadrilateral = (Quadrilateral?)Marshal.PtrToStructure(result.Value.location, typeof(Quadrilateral));
                        if (Quadrilateral != null)
                        {
                            DM_Point[] points = Quadrilateral.Value.points;
                            r.Points = new int[8] { points[0].coordinate[0], points[0].coordinate[1], points[1].coordinate[0], points[1].coordinate[1], points[2].coordinate[0], points[2].coordinate[1], points[3].coordinate[0], points[3].coordinate[1] };
                        }
                    }
                }
            }
        }
        DDN_FreeDetectedQuadResultArray(ref pResultArray);
    }

    return resultArray;
}
```

Conversely, we can copy managed memory to unmanaged memory by using `Marshal.StructureToPtr`. For example, we pass quadrilateral points to the C/C++ function `DDN_NormalizeFile`:

```csharp
Quadrilateral quad = new Quadrilateral();
quad.points = new DM_Point[4];
quad.points[0].coordinate = new int[2] { points[0], points[1] };
quad.points[1].coordinate = new int[2] { points[2], points[3] };
quad.points[2].coordinate = new int[2] { points[4], points[5] };
quad.points[3].coordinate = new int[2] { points[6], points[7] };

IntPtr pQuad = Marshal.AllocHGlobal(Marshal.SizeOf(quad));
Marshal.StructureToPtr(quad, pQuad, false);
int ret = DDN_NormalizeFile(handler, filename, "", pQuad, ref pResult);
```

### Custom Templates for Document Normalization
Dynamsoft Document Normalizer allows developers to customize parameters for document normalization algorithm and output results. Here we create three built-in templates for changing image color mode:

```csharp
public class Templates
{
    public static string binary = @"{
    ""GlobalParameter"":{
        ""Name"":""GP""
    },
    ""ImageParameterArray"":[
        {
            ""Name"":""IP-1"",
            ""NormalizerParameterName"":""NP-1""
        }
    ],
    ""NormalizerParameterArray"":[
        {
            ""Name"":""NP-1"",
            ""ColourMode"": ""ICM_BINARY"" 
        }
    ]
}";

    public static string color = @"{
    ""GlobalParameter"":{
        ""Name"":""GP""
    },
    ""ImageParameterArray"":[
        {
            ""Name"":""IP-1"",
            ""NormalizerParameterName"":""NP-1""
        }
    ],
    ""NormalizerParameterArray"":[
        {
            ""Name"":""NP-1"",
            ""ColourMode"": ""ICM_COLOUR"" 
        }
    ]
}";

    public static string grayscale = @"{
    ""GlobalParameter"":{
        ""Name"":""GP""
    },
    ""ImageParameterArray"":[
        {
            ""Name"":""IP-1"",
            ""NormalizerParameterName"":""NP-1""
        }
    ],
    ""NormalizerParameterArray"":[
        {
            ""Name"":""NP-1"",
            ""ColourMode"": ""ICM_GRAYSCALE"" 
        }
    ]
}";
}
```

For more advanced customization, please refer to the [online documentation](https://www.dynamsoft.com/document-normalizer/docs/parameters/parameter-organization-structure.html?ver=latest).


### Convert Binary Image to Grayscale Image
We save normalized Image to `NormalizedImage` class:

```csharp
public class NormalizedImage
{
    public int Width;
    public int Height;
    public int Stride;
    public ImagePixelFormat Format;
    public byte[] Data = new byte[0];

    public IntPtr _dataPtr = IntPtr.Zero;
}
```

To show the normalized image in GUI application, we can construct a `Bitmap` object from the `NormalizedImage` object. For grayscale and color images, we can construct a `Bitmap` directly by image stride, width, height, pixel format and bytes. However, for binary images, we need to convert the binary image to grayscale image first because the binary image saves 8 1-bit pixels in one byte.

```csharp
public byte[] Binary2Grayscale()
{
    byte[] data = new byte[Data.Length * 8];
    int index = 0;
    foreach (byte b in Data)
    {
        int byteCount = 7;
        while (byteCount >= 0)
        {
            int tmp = (b & (1 << byteCount)) >> byteCount;
            if (tmp == 1)
                data[index] = 255;
            else
                data[index] = 0;

            byteCount -= 1;
            index += 1;
        }
    }
    return data;
}
```

The final width of the grayscale image should be `Stride * 8` rather than using `Width`, for `Width` may be less than `Stride * 8`. The following code snippet shows how to convert the normalized image to OpenCV `Mat` object, and then convert the `Mat` object to `Bitmap` object for Windows Forms display:

```csharp
NormalizedImage image = scanner.NormalizeBuffer(_mat.Data, _mat.Cols, _mat.Rows, (int)_mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
if (image != null && image.Data != null)
{
    Mat mat2;
    if (image.Stride < image.Width)
    {
        // binary
        byte[] data = image.Binary2Grayscale();
        mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
    }
    else if (image.Stride >= image.Width * 3)
    {
        // color
        mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
    }
    else
    {
        // grayscale
        mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
    }
    pictureBox2.Image = BitmapConverter.ToBitmap(mat2);
}
```

## Command-line Tool for Detecting and Normalizing Documents
Now we can create a simple command-line document scanning tool for Windows and Linux. You must get a valid license key from [Dynamsoft customer portal](https://www.dynamsoft.com/customer/license/trialLicense?product=ddn) to make the app work.

```csharp
using System;
using System.Runtime.InteropServices;
using Dynamsoft;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            DocumentScanner.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="); // Get a license key from https://www.dynamsoft.com/customer/license/trialLicense?product=ddn
            Console.WriteLine("Version: " + DocumentScanner.GetVersionInfo());
            DocumentScanner scanner = DocumentScanner.Create();
            scanner.SetParameters(DocumentScanner.Templates.color);
            DocumentScanner.Result[]? resultArray = scanner.DetectFile("<image-file>");
            if (resultArray != null)
            {
                foreach (DocumentScanner.Result result in resultArray)
                {
                    Console.WriteLine("Confidence: " + result.Confidence);
                    if (result.Points != null)
                    {
                        foreach (int point in result.Points)
                        {
                            Console.WriteLine("Point: " + point);
                        }

                        DocumentScanner.NormalizedImage image = scanner.NormalizeFile("1.png", result.Points);
                        if (image != null)
                        {
                            image.Save(DateTime.Now.ToFileTimeUtc() + ".png");
                        }
                    }

                }
            }
        }
    }
}

```

The application only outputs the quadrilateral points of the detected document. You cannot see the effect until you save the normalized image to a file. To check the detection result straightforwardly, you can use OpenCV to display the normalized image.

Install OpenCvSharp4 package for Windows from NuGet:

```xml
<PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.5.5.20211231" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
```

For Linux, change the runtime package to `OpenCVSharp4.runtime.ubuntu.18.04-x64`.

Based on the code of the command-line tool, do some modifications to display the normalized image with OpenCV:

1. Call `DetectBuffer()` instead of `DetectFile()` to detect the document from a byte array. 
2. Call `NormalizeBuffer()` instead of `NormalizeFile()` to normalize the document from a byte array. 
3. Call `Cv2.ImShow()` to display the normalized image.

```csharp
using System;
using System.Runtime.InteropServices;
using Dynamsoft;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            DocumentScanner.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="); // Get a license key from https://www.dynamsoft.com/customer/license/trialLicense?product=ddn
            Console.WriteLine("Version: " + DocumentScanner.GetVersionInfo());
            DocumentScanner scanner = DocumentScanner.Create();
            scanner.SetParameters(DocumentScanner.Templates.binary);

            Mat mat = Cv2.ImRead("<image-file>", ImreadModes.Color);
            Mat copy = new Mat(mat.Rows, mat.Cols, MatType.CV_8UC3);
            mat.CopyTo(copy);
            
            DocumentScanner.Result[]? resultArray = scanner.DetectBuffer(copy.Data, copy.Cols, copy.Rows, (int)copy.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888);
            if (resultArray != null)
            {
                foreach (DocumentScanner.Result result in resultArray)
                {
                    Console.WriteLine("Confidence: " + result.Confidence);
                    if (result.Points != null)
                    {
                        Point[] points = new Point[4];
                        for (int i = 0; i < 4; i++)
                        {
                            points[i] = new Point(result.Points[i * 2], result.Points[i * 2 + 1]);
                        }
                        Cv2.DrawContours(mat, new Point[][] { points }, 0, Scalar.Red, 2);
                        Cv2.ImShow("Source Image", mat);

                        DocumentScanner.NormalizedImage image = scanner.NormalizeBuffer(mat.Data, mat.Cols, mat.Rows, (int)mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
                        if (image != null && image.Data != null)
                        {
                            Mat mat2;
                            if (image.Stride < image.Width) {
                                // binary
                                byte[] data = image.Binary2Grayscale();
                                mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
                            }
                            else if (image.Stride >= image.Width * 3) {
                                // color
                                mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
                            }
                            else {
                                // grayscale
                                mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
                            }
                            Cv2.ImShow("Normalized Document Image", mat2);
                            Cv2.WaitKey(0);
                            Cv2.DestroyAllWindows();
                            image.Save(DateTime.Now.ToFileTimeUtc() + ".png");
                        }
                    }

                }
            }
        }
    }
}

```

![.NET command-line document scanner](https://www.dynamsoft.com/codepool/img/2022/09/dotnet-document-scanner.png)


## Desktop Document Scanner with WinForms
If you want to do more UI operations, you can create a WinForms application. Use the form designer to add the UI elements.

![WinForms UI design for document scanner](https://www.dynamsoft.com/codepool/img/2022/09/winform-ui-design-document-scanner.png)

- There are two picture boxes: one for the source image and the other for the normalized image. 
- The `Load File` button is used to load an image file from the local file system.

    ```csharp
    using (OpenFileDialog dlg = new OpenFileDialog())
    {
        dlg.Title = "Open Image";
        dlg.Filter = "Image files (*.bmp, *.jpg, *.png) | *.bmp; *.jpg; *.png";

        if (dlg.ShowDialog() == DialogResult.OK)
        {
            try
            {
                _mat = Cv2.ImRead(dlg.FileName, ImreadModes.Color);
                Mat copy = new Mat(_mat.Rows, _mat.Cols, MatType.CV_8UC3);
                _mat.CopyTo(copy);
                pictureBox1.Image = DecodeMat(copy);
                PreviewNormalizedImage();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
    ```
- The `Camera Scan` button is used to open camera stream and scan documents in real-time. A worker thread is used to capture frames from the camera stream and detect documents. The detected document is normalized and displayed in the picture box.


    ```csharp
    private void StartScan() {
        button2.Text = "Stop";
        isCapturing = true;
        thread = new Thread(new ThreadStart(FrameCallback));
        thread.Start();
    }

    private void StopScan() {
        button2.Text = "Camera Scan";
        isCapturing = false;
        if (thread != null) thread.Join();
    }

    private void FrameCallback() {
        while (isCapturing) {
            capture.Read(_mat);
            Mat copy = new Mat(_mat.Rows, _mat.Cols, MatType.CV_8UC3);
            _mat.CopyTo(copy);
            pictureBox1.Image = DecodeMat(copy);
        }
    }
    ```
- The `Save Document` button is used to save normalized document images to the local file system. Considering there may be multiple documents detected from the source image, we call `FolderBrowserDialog()` to let the user select a folder to save the normalized document images.

    ```csharp
    FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();
    if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
    {
        if (_results != null)
        {
            foreach (Result result in _results)
            {
                NormalizedImage image = scanner.NormalizeBuffer(_mat.Data, _mat.Cols, _mat.Rows, (int)_mat.Step(), DocumentScanner.ImagePixelFormat.IPF_RGB_888, result.Points);
                if (image != null && image.Data != null)
                {
                    Mat mat2;
                    if (image.Stride < image.Width)
                    {
                        // binary
                        byte[] data = image.Binary2Grayscale();
                        mat2 = new Mat(image.Height, image.Stride * 8, MatType.CV_8UC1, data);
                    }
                    else if (image.Stride >= image.Width * 3)
                    {
                        // color
                        mat2 = new Mat(image.Height, image.Width, MatType.CV_8UC3, image.Data);
                    }
                    else
                    {
                        // grayscale
                        mat2 = new Mat(image.Height, image.Stride, MatType.CV_8UC1, image.Data);
                    }
                    image.Save(Path.Join(folderBrowserDialog.SelectedPath, DateTime.Now.ToFileTimeUtc() + _color + ".png"));
                }
            }

            MessageBox.Show("Saved normalized document images to " + folderBrowserDialog.SelectedPath);
        }
    }
    ```
- The template radio buttons are useful for checking the different effects of normalization algorithms.
    ```csharp
    scanner.SetParameters(DocumentScanner.Templates.binary);
    scanner.SetParameters(DocumentScanner.Templates.color);
    scanner.SetParameters(DocumentScanner.Templates.grayscale);
    scanner.SetParameters(richTextBox1.Text);
    ```

Finally, run the .NET desktop document scanner application.

```bash
dotnet run
```

![WinForms desktop .NET document scanner](https://www.dynamsoft.com/codepool/img/2022/09/dotnet-winform-document-scanner.png)

## Source Code
[https://github.com/yushulx/dotnet-document-scanner-sdk](https://github.com/yushulx/dotnet-document-scanner-sdk)