How to Build an ASP.NET Core App to Crop Document Images

ASP.NET Core is a cross-platform, high-performance, open-source framework for building modern, cloud-enabled, Internet-connected apps. It is convenient to use it to build web APIs and web apps. In this article, we are going build an ASP.NET app to crop document images.

It mainly provides the following API:

  • /api/document/detect: upload an image and get the detected document boundaries
  • /api/document/crop: pass the document boundaries and crop the document image uploaded
  • /api/document/detectAndCrop: upload an image and get the cropped document image’s ID
  • /api/document/{ID}: get the uploaded image based on its ID
  • /api/document/cropped/{ID}: get the cropped image based on its ID

A front-end is also written to use the API.

Front-end screenshot

New Project

Create a new ASP.NET Core Web App project with Visual Studio.

new project

Add Dependencies

Install Dynamsoft Document Normalizer from nuget. We are going to use it to detect document boundaries and get cropped document images.

dotnet add package Dynamsoft.DotNet.DocumentNormalizer.Bundle --version 2.2.1000

License Initialization

In Program.cs, init the license for Dynamsoft Document Normalizer. You can apply for your license here.

using Dynamsoft.Core;
using Dynamsoft.License;

string errorMsg;
int errorCode = LicenseManager.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", out errorMsg); //use a one-day trial license
if (errorCode != (int)EnumErrorCode.EC_OK)
{
    Console.WriteLine("License initialization error: " + errorMsg);
}
else 
{
    Console.WriteLine("License valid.");
}

Implement the API

  1. Add a new API controller named DocumentController.cs and create a capture vision router property to call Dynamsoft Document Normalizer.

    namespace DocumentScanner
    {
        [Route("api/[controller]")]
        [ApiController]
        public class DocumentController : ControllerBase
        {
            private CaptureVisionRouter cvr = new CaptureVisionRouter();
        }
    }
    

    In Program.cs, map controllers.

    app.MapControllers();
    
  2. Define related classes for requests.

    Polygon and Point which are to represent the detected document boundaries.

    public class Polygon
    {
        public Point[] Points { get; set; }
        public Polygon() { 
            Points = Array.Empty<Point>();
        }
    }
       
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }
        public Point(int x, int y)
        {
            X = x;
            Y = y;
        }
    }
    

    Document which is used to represent a document. It can contain the document boundaries, the base64-encoded image data, the ID used to store the image on the disk and the success status.

    public class Document
    {
        public string? Base64 { get; set; }
        public Polygon? Polygon { get; set; }
        public bool? Success { get; set; }
        public long? ID { get; set; }
    }
    
  3. Add a method to detect the document boundaries.

    [HttpPost("detect")]
    public ActionResult<Document> DetectDocument(Document document)
    {
        Document detectedDocument = new Document();
        if (document.Base64 != null) {
            byte[] bytes = Convert.FromBase64String(document.Base64);
            CapturedResult result = cvr.Capture(bytes, PresetTemplate.PT_DETECT_DOCUMENT_BOUNDARIES);
            DetectedQuadsResult quads = result.GetDetectedQuadsResult();
            if (quads != null && quads.GetItems().Length > 0) {
                Polygon polygon = ConvertToPolygon(quads.GetItems()[0].GetLocation());
                detectedDocument.Polygon = polygon;
            }
            long ID = SaveImage(bytes);
            detectedDocument.ID = ID;
            detectedDocument.Success = true;
        }
        return detectedDocument;
    }
       
    private Polygon ConvertToPolygon(Quadrilateral quad) {
        Polygon polygon = new Polygon();
        Point[] points = new Point[4];
        points[0] = new Point(quad.points[0][0], quad.points[0][1]);
        points[1] = new Point(quad.points[1][0], quad.points[1][1]);
        points[2] = new Point(quad.points[2][0], quad.points[2][1]);
        points[3] = new Point(quad.points[3][0], quad.points[3][1]);
        polygon.Points = points;
        return polygon;
    }
       
    private long SaveImage(byte[] bytes) {
        if (Directory.Exists("images") == false) {
            Directory.CreateDirectory("images");
        }
        DateTimeOffset dateTimeOffset = DateTimeOffset.UtcNow;
        long ID = dateTimeOffset.ToUnixTimeMilliseconds();
        string filePath;
        filePath = "./images/" + ID + ".jpg";
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            fs.Write(bytes, 0, bytes.Length);
        }
        return ID;
    }
    
  4. Add a method to crop the document image with a specified document boundary.

    [HttpPost("crop")]
    public ActionResult<Document> CropDocument(Document document)
    {
        Document croppedDocument = new Document();
        if (document.ID != null && document.Polygon != null)
        {
            string filePath = "./images/" + document.ID + ".jpg";
            SimplifiedCaptureVisionSettings settings;
            cvr.GetSimplifiedSettings(PresetTemplate.PT_NORMALIZE_DOCUMENT, out settings);
            settings.roiMeasuredInPercentage = 0;
            settings.roi = ConvertToQuad(document.Polygon);
            string errorMsg;
            cvr.UpdateSettings(PresetTemplate.PT_NORMALIZE_DOCUMENT, settings, out errorMsg);
            CapturedResult result = cvr.Capture(filePath, PresetTemplate.PT_NORMALIZE_DOCUMENT);
            NormalizedImagesResult normalizedImagesResult = result.GetNormalizedImagesResult();
            if (normalizedImagesResult.GetItems().Length > 0)
            {
                ImageData imageData = normalizedImagesResult.GetItems()[0].GetImageData();
                ImageManager imageManager = new ImageManager();
                if (imageData != null)
                {
                    int errorCode = imageManager.SaveToFile(imageData, "./images/" + document.ID + "-cropped.jpg");
                    if (errorCode == 0) {
                        croppedDocument.ID = document.ID;
                        croppedDocument.Success = true;
                    }
                }
            }
        }
        return croppedDocument;
    }
       
    private Quadrilateral ConvertToQuad(Polygon polygon)
    {
        Quadrilateral quadrilateral = new Quadrilateral();
        quadrilateral.points[0] = new Dynamsoft.Core.Point(polygon.Points[0].X, polygon.Points[0].Y);
        quadrilateral.points[1] = new Dynamsoft.Core.Point(polygon.Points[1].X, polygon.Points[1].Y);
        quadrilateral.points[2] = new Dynamsoft.Core.Point(polygon.Points[2].X, polygon.Points[2].Y);
        quadrilateral.points[3] = new Dynamsoft.Core.Point(polygon.Points[3].X, polygon.Points[3].Y);
        return quadrilateral;
    }
    
  5. Add a method to detect the document boundaries and crop the document images.

    [HttpPost("detectAndCrop")]
    public ActionResult<Document> DetectAndCropDocument(Document document)
    {
        Document croppedDocument = new Document();
        if (document.Base64 != null)
        {
            byte[] bytes = Convert.FromBase64String(document.Base64);
            CapturedResult result = cvr.Capture(bytes, PresetTemplate.PT_DETECT_AND_NORMALIZE_DOCUMENT);
            NormalizedImagesResult normalizedImagesResult = result.GetNormalizedImagesResult();
            if (normalizedImagesResult != null && normalizedImagesResult.GetItems().Length > 0)
            {
                DateTimeOffset dateTimeOffset = DateTimeOffset.UtcNow;
                long ID = dateTimeOffset.ToUnixTimeMilliseconds();
                ImageData imageData = normalizedImagesResult.GetItems()[0].GetImageData();
                ImageManager imageManager = new ImageManager();
                if (imageData != null)
                {
                    int errorCode = imageManager.SaveToFile(imageData, "./images/" + ID + "-cropped.jpg");
                    if (errorCode == 0)
                    {
                        croppedDocument.ID = ID;
                        croppedDocument.Success = true;
                    }
                }
            }
        }
        return croppedDocument;
    }
    
  6. Add methods to get the uploaded and cropped images.

    [HttpGet("{ID}")]
    public ActionResult<string> GetDocument(long ID)
    {
        string filePath = "./images/"+ID+".jpg";
        Response.ContentType = "text/plain";
        if (System.IO.File.Exists(filePath))
        {
            byte[] bytes = System.IO.File.ReadAllBytes(filePath);
            string base64String = Convert.ToBase64String(bytes);
            Response.StatusCode = 200;
            return base64String;
        }
        else {
            Response.StatusCode = 404;
            return "not found";
        }
    }
    
    [HttpGet("cropped/{ID}")]
    public ActionResult<string> GetCroppedDocument(long ID)
    {
        string filePath = "./images/" + ID + "-cropped.jpg";
        Response.ContentType = "text/plain";
        if (System.IO.File.Exists(filePath))
        {
            byte[] bytes = System.IO.File.ReadAllBytes(filePath);
            string base64String = Convert.ToBase64String(bytes);
            Response.StatusCode = 200;
            return base64String;
        }
        else
        {
            Response.StatusCode = 404;
            return "not found";
        }
    }
    

Have a Test

We can then write a web page to test the API using AJAX:

let url = "/api/document/detectAndCrop";
let dataURL = document.getElementById("original").src; //an image element's dataURL
let base64 = dataURL.substring(dataURL.indexOf(",")+1,dataURL.length);
data = {Base64:base64};
const response = await fetch(url, {
  method: "POST", 
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});
let json = await response.json();

let imageID = json.id;
if (json.success == true) {
  let response = await fetch("/api/document/cropped/"+imageID);
  let base64 = await response.text();
  displayCropped(base64);
}else{
  alert("Failed to get the cropped Image.");
}

You can find a more full-featured sample which can edit the detected polygon in the source code.

Enable CORS

If we need to make the API callable from a web app in a different origin, we have to enable CORS.

  1. Enable CORS in Program.cs.

    builder.Services.AddCors();
    app.UseCors(builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
    
  2. If we need to access it via a private network, we also need to add the Access-Control-Allow-Private-Network header.

    // Enable PNA preflight requests
    app.Use(async (ctx, next) =>
    {
        if (ctx.Request.Method.Equals("options", StringComparison.InvariantCultureIgnoreCase) && ctx.Request.Headers.ContainsKey("Access-Control-Request-Private-Network"))
        {
            ctx.Response.Headers.Add("Access-Control-Allow-Private-Network", "true");
        }
    
        await next();
    });
    

Source Code

The source code of the project is available here: https://github.com/tony-xlh/Capture-Vision-Server-Demos/tree/main/Document-Normalizer/ASP.NET