Building Node.js C++ Addon for Document Edge Detection and Rectification

A Node.js C++ addon is a module written in C++ that can be loaded into Node.js, used to extend Node.js capabilities by allowing developers to implement functionality in C++. This article will guide you through building a Node.js C++ addon for document edge detection and rectification. The addon relies on the Dynamsoft Document Normalizer C++ SDK, which offers APIs for detecting document edges and normalizing the document using the identified quadrilaterals.

NPM Package

https://www.npmjs.com/package/docrectifier4nodejs

Target Platforms

  • Windows
  • Linux

Prerequisites

  • Node.js
  • C/C++ compiler for Windows or Linux
  • node-gyp

      npm i node-gyp -g
    

    node-gyp is a cross-platform, Node.js-based command-line tool designed for compiling native addon modules specifically for Node.js.

  • Dynamsoft Document Normalizer C++ SDK for Windows and Linux
  • A 30-day FREE trial license for Dynamsoft Document Normalizer C++ SDK. You can get it from here.

Node.js C++ Addon Project Structure

The basic structure of a Node.js C++ addon project is as follows:

my-addon/
├── binding.gyp
├── package.json
├── src/
│   ├── my_addon.cc
│   └── my_addon.h
└── index.js
  • binding.gyp - The build configuration file for the addon.
  • package.json - The package configuration file for the addon.
  • src/ - The C++ source code directory for the addon.
  • index.js - The entry point of the addon.

Implement C++ Methods for Node.js Addon

Let’s get started with the header file:

#ifndef DocRectifierSCANNER_H
#define DocRectifierSCANNER_H

#include <node.h>
#include <node_object_wrap.h>

#include "DynamsoftCore.h"
#include "DynamsoftDocumentNormalizer.h"

class DocRectifier : public node::ObjectWrap
{
public:
  static void Init(v8::Local<v8::Object> exports);
  void *handler;
  NormalizedImageResult *imageResult;

  void FreeImage();

private:
  explicit DocRectifier();
  ~DocRectifier();

  static void New(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void GetParameters(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void SetParameters(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void Save(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void DetectFileAsync(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void DetectBufferAsync(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void NormalizeBufferAsync(const v8::FunctionCallbackInfo<v8::Value> &args);
  static void NormalizeFileAsync(const v8::FunctionCallbackInfo<v8::Value> &args);
};

#endif
  • The DynamsoftCore.h and DynamsoftDocumentNormalizer.h are the header files of Dynamsoft Document Normalizer C++ SDK.
  • The DocRectifier class is derived from node::ObjectWrap. It is used to wrap the native C++ object and expose it to JavaScript.
  • The void *handler is the handler of the DynamsoftDocumentNormalizer object.
  • The NormalizedImageResult *imageResult is the pointer of the rectified document image.

Following that, we’ll move to the cpp file to implement the code.

The native entry point of the addon is the Init method, which is responsible for exporting native methods to JavaScript.

void Init(Local<Object> exports)
{
	NODE_SET_METHOD(exports, "initLicense", InitLicense);
	NODE_SET_METHOD(exports, "getVersionNumber", GetVersionNumber);
	DocRectifier::Init(exports);
}

NODE_MODULE(DocRectifier, Init)

The initLicense and getVersionNumber are global methods and not encapsulated within the DocRectifier class.

void InitLicense(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();
	Local<Context> context = isolate->GetCurrentContext();

	String::Utf8Value license(isolate, args[0]);
	char *pszLicense = *license;
	char errorMsgBuffer[512];
	int ret = DC_InitLicense(pszLicense, errorMsgBuffer, 512);
	args.GetReturnValue().Set(Number::New(isolate, ret));
}

void GetVersionNumber(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = Isolate::GetCurrent();
	args.GetReturnValue().Set(String::NewFromUtf8(
								  isolate, DDN_GetVersion())
								  .ToLocalChecked());
}

The DocRectifier::Init(exports) method exports the DocRectifier class and its associated methods to JavaScript.

void DocRectifier::Init(Local<Object> exports)
{
	Isolate *isolate = exports->GetIsolate();
	Local<Context> context = isolate->GetCurrentContext();

	Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
	addon_data_tpl->SetInternalFieldCount(1); 
	Local<Object> addon_data =
		addon_data_tpl->NewInstance(context).ToLocalChecked();

	Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
	tpl->SetClassName(String::NewFromUtf8(isolate, "DocRectifier").ToLocalChecked());
	tpl->InstanceTemplate()->SetInternalFieldCount(1);

	NODE_SET_PROTOTYPE_METHOD(tpl, "getParameters", GetParameters);
	NODE_SET_PROTOTYPE_METHOD(tpl, "setParameters", SetParameters);
	NODE_SET_PROTOTYPE_METHOD(tpl, "save", Save);
	NODE_SET_PROTOTYPE_METHOD(tpl, "detectFileAsync", DetectFileAsync);
	NODE_SET_PROTOTYPE_METHOD(tpl, "detectBufferAsync", DetectBufferAsync);
	NODE_SET_PROTOTYPE_METHOD(tpl, "normalizeBufferAsync", NormalizeBufferAsync);
	NODE_SET_PROTOTYPE_METHOD(tpl, "normalizeFileAsync", NormalizeFileAsync);

	Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
	addon_data->SetInternalField(0, constructor);
	exports->Set(context, String::NewFromUtf8(isolate, "DocRectifier").ToLocalChecked(),
				 constructor)
		.FromJust();
}

void DocRectifier::New(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();
	Local<Context> context = isolate->GetCurrentContext();

	if (args.IsConstructCall())
	{
		DocRectifier *obj = new DocRectifier();
		obj->Wrap(args.This());
		args.GetReturnValue().Set(args.This());
	}
	else
	{
		const int argc = 1;
		Local<Value> argv[argc] = {args[0]};
		Local<Function> cons =
			args.Data().As<Object>()->GetInternalField(0).As<Function>();
		Local<Object> result =
			cons->NewInstance(context, argc, argv).ToLocalChecked();
		args.GetReturnValue().Set(result);
	}
}

DocRectifier::DocRectifier()
{
	handler = DDN_CreateInstance();
	imageResult = NULL;
}

DocRectifier::~DocRectifier()
{
	DDN_DestroyInstance(handler);

	FreeImage();
}

void DocRectifier::FreeImage()
{
	if (imageResult != NULL)
	{
		DDN_FreeNormalizedImageResult(&imageResult);
		imageResult = NULL;
	}
}

The DocRectifier class comprises three synchronous methods and four asynchronous methods. To facilitate non-blocking operations, the asynchronous methods employ libuv’s thread pool through the invocation of uv_queue_work. Additionally, two worker structs are defined for data transfer between the main thread and the worker thread.

struct DetectWorker
{
	void *handler;
	uv_work_t request;				   
	Persistent<Function> callback;	  
	char filename[128];				   
	DetectedQuadResultArray *pResults; 
	unsigned char *buffer;
	int size;			  
	int errorCode;		   
	int width;			  
	int height;			   
	BufferType bufferType; 
	int stride;			   
};

struct NormalizeWorker
{
	DocRectifier *obj;
	uv_work_t request;			  
	Persistent<Function> callback; 
	char filename[128];			   
	unsigned char *buffer;
	int size;			  
	int errorCode;		   
	int width;			   
	int height;			  
	BufferType bufferType; 
	int stride;			   
	int x1, y1, x2, y2, x3, y3, x4, y4;
};

The Save() method, a synchronous function, is responsible for saving the rectified document image to a file.

void DocRectifier::Save(const FunctionCallbackInfo<Value> &args)
{

	Isolate *isolate = args.GetIsolate();
	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());
	String::Utf8Value fileName(isolate, args[0]);

	int ret = DMERR_UNKNOWN;
	if (obj->imageResult)
	{
		ret = NormalizedImageResult_SaveToFile(obj->imageResult, *fileName);
		if (ret != DM_OK)
			printf("NormalizedImageResult_SaveToFile: %s\r\n", DC_GetErrorString(ret));
	}

	args.GetReturnValue().Set(Number::New(isolate, ret));
}

The GetParameters() method, a synchronous function, is responsible for getting the current parameters of the DynamsoftDocumentNormalizer object.

void DocRectifier::GetParameters(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();

	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());

	char *content = NULL;
	DDN_OutputRuntimeSettingsToString(obj->handler, "", &content);

	args.GetReturnValue().Set(String::NewFromUtf8(
								  isolate, content)
								  .ToLocalChecked());

	if (content != NULL)
		DDN_FreeString(&content);
}

The SetParameters(), a synchronous function, is responsible for setting the parameters of the DynamsoftDocumentNormalizer object.

void DocRectifier::SetParameters(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();
	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());

	String::Utf8Value params(isolate, args[0]);
	char errorMsgBuffer[512];
	int ret = DDN_InitRuntimeSettingsFromString(obj->handler, *params, errorMsgBuffer, 512);
	if (ret != DM_OK)
	{
		printf("%s\n", errorMsgBuffer);
	}

	args.GetReturnValue().Set(Number::New(isolate, ret));
}

The DetectFileAsync() and DetectBufferAsync() methods are asynchronous. They detect the edges of a document image and return the coordinates of the found quadrilaterals.

void DocRectifier::DetectFileAsync(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();
	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());
	Local<Context> context = isolate->GetCurrentContext();

	String::Utf8Value fileName(isolate, args[0]); 
	char *pFileName = *fileName;
	Local<Function> cb = Local<Function>::Cast(args[1]); 

	DetectWorker *worker = new DetectWorker;
	worker->handler = obj->handler;
	worker->request.data = worker;
	strcpy(worker->filename, pFileName);
	worker->callback.Reset(isolate, cb);
	worker->pResults = NULL;
	worker->buffer = NULL;
	worker->bufferType = NO_BUFFER;

	uv_queue_work(uv_default_loop(), &worker->request, (uv_work_cb)DetectionWorking, (uv_after_work_cb)DetectionDone);
}

The DetectionWorking() method is the worker thread function, which is responsible for detecting the edges of a document image.

static void DetectionWorking(uv_work_t *req)
{
	DetectWorker *worker = static_cast<DetectWorker *>(req->data);
	if (!worker->handler)
	{
		printf("DocRectifier handler not initialized.\n");
		return;
	}

	DetectedQuadResultArray *pResults = NULL;

	int ret = 0;
	switch (worker->bufferType)
	{
	case RGB_BUFFER:
	{
		if (worker->buffer)
		{
			int width = worker->width, height = worker->height, stride = worker->stride;
			ImagePixelFormat format = IPF_RGB_888;

			if (width == stride)
			{
				format = IPF_GRAYSCALED;
			}
			else if (((width * 3 + 3) & ~3) == stride)
			{
				format = IPF_RGB_888;
			}
			else if (width * 4 == stride)
			{
				format = IPF_ARGB_8888;
			}

			ImageData data;
			data.bytes = worker->buffer;
			data.width = width;
			data.height = height;
			data.stride = stride;
			data.format = format;
			data.bytesLength = stride * height;
			data.orientation = 0;

			ret = DDN_DetectQuadFromBuffer(worker->handler, &data, "", &pResults);
		}
		break;
	}
	default:
	{
		ret = DDN_DetectQuadFromFile(worker->handler, worker->filename, "", &pResults);
	}
	}

	if (ret)
	{
		printf("Detection error: %s\n", DC_GetErrorString(ret));
	}

	worker->errorCode = ret;
	worker->pResults = pResults;
}

The DetectionDone() method serves as the callback function for the worker thread. It is triggered upon the completion of the worker thread’s task, returning the coordinates of the detected quadrilaterals.

static void DetectionDone(uv_work_t *req, int status)
{
	Isolate *isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);
	Local<Context> context = isolate->GetCurrentContext();

	DetectWorker *worker = static_cast<DetectWorker *>(req->data);

	DetectedQuadResultArray *pResults = worker->pResults;
	int errorCode = worker->errorCode;

	Local<Array> documentResults = Array::New(isolate);

	if (pResults)
	{
		int count = pResults->resultsCount;
		for (int i = 0; i < count; i++)
		{
			DetectedQuadResult *quadResult = pResults->detectedQuadResults[i];
			int confidence = quadResult->confidenceAsDocumentBoundary;
			DM_Point *points = quadResult->location->points;
			int x1 = points[0].coordinate[0];
			int y1 = points[0].coordinate[1];
			int x2 = points[1].coordinate[0];
			int y2 = points[1].coordinate[1];
			int x3 = points[2].coordinate[0];
			int y3 = points[2].coordinate[1];
			int x4 = points[3].coordinate[0];
			int y4 = points[3].coordinate[1];

			Local<Object> result = Object::New(isolate);
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "confidence", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, confidence));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "x1", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, x1));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "y1", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, y1));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "x2", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, x2));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "y2", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, y2));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "x3", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, x3));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "y3", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, y3));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "x4", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, x4));
			result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "y4", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, y4));
			documentResults->Set(context, Number::New(isolate, i), result);
		}

		DDN_FreeDetectedQuadResultArray(&pResults);
	}

	const unsigned argc = 2;
	Local<Number> err = Number::New(isolate, errorCode);
	Local<Value> argv[argc] = {err, documentResults};
	Local<Function> cb = Local<Function>::New(isolate, worker->callback);
	cb->Call(context, Null(isolate), argc, argv);

	delete worker;
}

The NormalizeFileAsync() and NormalizeBufferAsync() methods are asynchronous. They rectify the document image based on the found quadrilaterals.

void DocRectifier::NormalizeBufferAsync(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = Isolate::GetCurrent();
	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());
	Local<Context> context = isolate->GetCurrentContext();

	unsigned char *buffer = (unsigned char *)node::Buffer::Data(args[0]);
	int width = args[1]->Int32Value(context).ToChecked();
	int height = args[2]->Int32Value(context).ToChecked();
	int stride = args[3]->Int32Value(context).ToChecked();
	int x1 = args[4]->Int32Value(context).ToChecked();
	int y1 = args[5]->Int32Value(context).ToChecked();
	int x2 = args[6]->Int32Value(context).ToChecked();
	int y2 = args[7]->Int32Value(context).ToChecked();
	int x3 = args[8]->Int32Value(context).ToChecked();
	int y3 = args[9]->Int32Value(context).ToChecked();
	int x4 = args[10]->Int32Value(context).ToChecked();
	int y4 = args[11]->Int32Value(context).ToChecked();
	Local<Function> cb = Local<Function>::Cast(args[12]);

	NormalizeWorker *worker = new NormalizeWorker;
	worker->request.data = worker;
	worker->callback.Reset(isolate, cb);
	worker->buffer = buffer;
	worker->width = width;
	worker->height = height;
	worker->bufferType = RGB_BUFFER;
	worker->stride = stride;
	worker->obj = obj;
	worker->x1 = x1;
	worker->y1 = y1;
	worker->x2 = x2;
	worker->y2 = y2;
	worker->x3 = x3;
	worker->y3 = y3;
	worker->x4 = x4;
	worker->y4 = y4;

	uv_queue_work(uv_default_loop(), &worker->request, (uv_work_cb)NormalizeWorking, (uv_after_work_cb)NormalizeDone);
}

void DocRectifier::NormalizeFileAsync(const FunctionCallbackInfo<Value> &args)
{
	Isolate *isolate = args.GetIsolate();
	DocRectifier *obj = ObjectWrap::Unwrap<DocRectifier>(args.Holder());
	Local<Context> context = isolate->GetCurrentContext();

	String::Utf8Value fileName(isolate, args[0]); 
	char *pFileName = *fileName;
	int x1 = args[1]->Int32Value(context).ToChecked();
	int y1 = args[2]->Int32Value(context).ToChecked();
	int x2 = args[3]->Int32Value(context).ToChecked();
	int y2 = args[4]->Int32Value(context).ToChecked();
	int x3 = args[5]->Int32Value(context).ToChecked();
	int y3 = args[6]->Int32Value(context).ToChecked();
	int x4 = args[7]->Int32Value(context).ToChecked();
	int y4 = args[8]->Int32Value(context).ToChecked();
	Local<Function> cb = Local<Function>::Cast(args[9]);
	NormalizeWorker *worker = new NormalizeWorker;
	worker->request.data = worker;
	strcpy(worker->filename, pFileName);
	worker->callback.Reset(isolate, cb);
	worker->buffer = NULL;
	worker->bufferType = NO_BUFFER;
	worker->obj = obj;
	worker->x1 = x1;
	worker->y1 = y1;
	worker->x2 = x2;
	worker->y2 = y2;
	worker->x3 = x3;
	worker->y3 = y3;
	worker->x4 = x4;
	worker->y4 = y4;

	uv_queue_work(uv_default_loop(), &worker->request, (uv_work_cb)NormalizeWorking, (uv_after_work_cb)NormalizeDone);
}

The NormalizeWorking() method is the worker thread function, which is responsible for rectifying the document image based on the found quadrilaterals.

static void NormalizeWorking(uv_work_t *req)
{
	NormalizeWorker *worker = static_cast<NormalizeWorker *>(req->data);
	if (!worker->obj->handler)
	{
		printf("DocRectifier handler not initialized.\n");
		return;
	}

	worker->obj->FreeImage();

	Quadrilateral quad;
	quad.points[0].coordinate[0] = worker->x1;
	quad.points[0].coordinate[1] = worker->y1;
	quad.points[1].coordinate[0] = worker->x2;
	quad.points[1].coordinate[1] = worker->y2;
	quad.points[2].coordinate[0] = worker->x3;
	quad.points[2].coordinate[1] = worker->y3;
	quad.points[3].coordinate[0] = worker->x4;
	quad.points[3].coordinate[1] = worker->y4;

	int ret = 0;
	switch (worker->bufferType)
	{
	case RGB_BUFFER:
	{
		if (worker->buffer)
		{
			int width = worker->width, height = worker->height, stride = worker->stride;
			ImagePixelFormat format = IPF_RGB_888;

			if (width == stride)
			{
				format = IPF_GRAYSCALED;
			}
			else if (((width * 3 + 3) & ~3) == stride)
			{
				format = IPF_RGB_888;
			}
			else if (width * 4 == stride)
			{
				format = IPF_ARGB_8888;
			}

			ImageData data;
			data.bytes = worker->buffer;
			data.width = width;
			data.height = height;
			data.stride = stride;
			data.format = format;
			data.bytesLength = stride * height;
			data.orientation = 0;

			ret = DDN_NormalizeBuffer(worker->obj->handler, &data, "", &quad, &worker->obj->imageResult);
		}
		break;
	}
	default:
	{
		ret = DDN_NormalizeFile(worker->obj->handler, worker->filename, "", &quad, &worker->obj->imageResult);
	}
	}

	if (ret)
	{
		printf("Detection error: %s\n", DC_GetErrorString(ret));
	}

	worker->errorCode = ret;
}

The NormalizeDone() serves as the callback function for the worker thread. It is triggered upon the completion of the worker thread’s task, returning the rectified document image.

static void NormalizeDone(uv_work_t *req, int status)
{
	Isolate *isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);
	Local<Context> context = isolate->GetCurrentContext();

	NormalizeWorker *worker = static_cast<NormalizeWorker *>(req->data);

	int errorCode = worker->errorCode;
	Local<Object> result = Object::New(isolate);

	if (worker->obj->imageResult)
	{
		ImageData *imageData = worker->obj->imageResult->image;
		int width = imageData->width;
		int height = imageData->height;
		int stride = imageData->stride;
		int format = (int)imageData->format;
		unsigned char *data = imageData->bytes;
		int orientation = imageData->orientation;
		int length = imageData->bytesLength;

		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "width", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, width));
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "height", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, height));
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "stride", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, stride));
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "format", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, format));
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "orientation", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, orientation));
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Number::New(isolate, length));

		unsigned char *rgba = (unsigned char *)calloc(width * height * 4, sizeof(unsigned char));

		if (format == IPF_RGB_888)
		{
			int dataIndex = 0;
			for (int i = 0; i < height; i++)
			{
				for (int j = 0; j < width; j++)
				{
					int index = i * width + j;

					rgba[index * 4] = data[dataIndex];		   
					rgba[index * 4 + 1] = data[dataIndex + 1]; 
					rgba[index * 4 + 2] = data[dataIndex + 2]; 
					rgba[index * 4 + 3] = 255;				  
					dataIndex += 3;
				}
			}
		}
		else if (format == IPF_GRAYSCALED)
		{
			int dataIndex = 0;
			for (int i = 0; i < height; i++)
			{
				for (int j = 0; j < width; j++)
				{
					int index = i * width + j;
					rgba[index * 4] = data[dataIndex];
					rgba[index * 4 + 1] = data[dataIndex];
					rgba[index * 4 + 2] = data[dataIndex];
					rgba[index * 4 + 3] = 255;
					dataIndex += 1;
				}
			}
		}
		else if (format == IPF_BINARY)
		{
			unsigned char *grayscale = (unsigned char *)calloc(width * height, sizeof(unsigned char));
			binary2grayscale(data, grayscale, width, height, stride, length);

			int dataIndex = 0;
			for (int i = 0; i < height; i++)
			{
				for (int j = 0; j < width; j++)
				{
					int index = i * width + j;
					rgba[index * 4] = grayscale[dataIndex];
					rgba[index * 4 + 1] = grayscale[dataIndex];
					rgba[index * 4 + 2] = grayscale[dataIndex];
					rgba[index * 4 + 3] = 255;
					dataIndex += 1;
				}
			}

			free(grayscale);
		}

		std::vector<uint8_t> rawBytes(rgba, rgba + width * height * 4);
		result->DefineOwnProperty(context, String::NewFromUtf8(isolate, "data", NewStringType::kNormal).ToLocalChecked(), node::Buffer::Copy(isolate, (char *)rawBytes.data(), rawBytes.size()).ToLocalChecked());
		free(rgba);
	}

	const unsigned argc = 2;
	Local<Number> err = Number::New(isolate, errorCode);
	Local<Value> argv[argc] = {err, result};
	Local<Function> cb = Local<Function>::New(isolate, worker->callback);
	cb->Call(context, Null(isolate), argc, argv);

	delete worker;
}

Configure Binding.gyp for Compiling Node.js C++ Addon

In the binding.gyp file, we define the specifications for library linking and file copying across various platforms.

'conditions': [
    ['OS=="linux"', {
        'defines': [
            'LINUX_DOCRECTIFIER',
        ],

        "cflags": [
            "-std=c++11"
        ],
        'ldflags': [
            "-Wl,-rpath,'$$ORIGIN'"
        ],

        'libraries': [
            "-lDynamsoftCore", "-lDynamsoftDocumentNormalizer", "-L../lib/linux"
        ],
        'copies': [
            {
                'destination': 'build/Release/',
                'files': [
                    './lib/linux/libDynamicImage.so',
                    './lib/linux/libDynamicPdf.so',
                    './lib/linux/libDynamicPdfCore.so',
                    './lib/linux/libDynamsoftCore.so',
                    './lib/linux/libDynamsoftDocumentNormalizer.so',
                    './lib/linux/libDynamsoftImageProcessing.so',
                    './lib/linux/libDynamsoftIntermediateResult.so',
                ]
            }
        ]
    }],
    ['OS=="win"', {
        'defines': [
            'WINDOWS_DOCRECTIFIER',
        ],
        'libraries': [
            "-l../lib/windows/DynamsoftCorex64.lib", "-l../lib/windows/DynamsoftDocumentNormalizerx64.lib"
        ],
        'copies': [
            {
                'destination': 'build/Release/',
                'files': [
                    './lib/windows/DynamicImagex64.dll',
                    './lib/windows/DynamicPdfCorex64.dll',
                    './lib/windows/DynamicPdfx64.dll',
                    './lib/windows/DynamsoftCorex64.dll',
                    './lib/windows/DynamsoftDocumentNormalizerx64.dll',
                    './lib/windows/DynamsoftImageProcessingx64.dll',
                    './lib/windows/DynamsoftIntermediateResultx64.dll',
                    './lib/windows/vcomp140.dll',
                ]
            }
        ],
    }]
]

Export the Native Methods to JavaScript

The index.js file acts as the entry point for the Node.js addon, responsible for exporting the native methods to JavaScript.

var docrectifier = require('./build/Release/docrectifier');
var DocRectifier = /** @class */ (function () {
    function DocRectifier() {
        this.obj = docrectifier.DocRectifier();
    }
    DocRectifier.initLicense = function (license) {
        return docrectifier.initLicense(license);
    };
    DocRectifier.getVersionNumber = function () {
        return docrectifier.getVersionNumber();
    };
    DocRectifier.prototype.save = function (filePath) {
        var _this = this;
        return _this.obj.save(filePath);
    };
    DocRectifier.prototype.getParameters = function () {
        var _this = this;
        return _this.obj.getParameters();
    };
    DocRectifier.prototype.setParameters = function (params) {
        var _this = this;
        return _this.obj.setParameters(params);
    };
    DocRectifier.prototype.detectFileAsync = function () {
        var _this = this;
        var filename = arguments[0];
        var callback = arguments[1];
        const promise = new Promise((resolve, reject) => {
            _this.obj.detectFileAsync(filename, function (err, result) {
                setTimeout(function () {
                    if (err) {
                        reject(err);
                    }
                    else {
                        resolve(result);
                    }
                }, 0);
            });
        });

        if (callback && typeof callback === 'function') {
            promise
                .then(result => callback(null, result))
                .catch(err => callback(err));
        } else {
            return promise;
        }
    };
    DocRectifier.prototype.detectBufferAsync = function () {
        ...
    };
    DocRectifier.prototype.normalizeFileAsync = function () {
        ...
    };
    DocRectifier.prototype.normalizeBufferAsync = function () {
        ...
    };
    DocRectifier.Template = Template;
    return DocRectifier;
}());
module.exports = DocRectifier;

The asynchronous methods are designed to support both callback functions and promises.

Node.js Examples for Document Rectification

In the subsequent sections, we will develop two examples to illustrate the usage of the Node.js C++ addon for document edge detection and rectification.

npm install docrectifier4nodejs

Command Line

Set the LICENSE-KEY and specify the image-file in the code below:

const DocRectifier = require('docrectifier4nodejs');
console.log(DocRectifier.getVersionNumber());
DocRectifier.initLicense('LICENSE-KEY');

var obj = new DocRectifier();
(async function () {
    try {
        obj.setParameters(DocRectifier.Template.color);
        let results = await obj.detectFileAsync('image-file');
        console.log(results);
        let result = results[0];
        result = await obj.normalizeFileAsync('image-file', result['x1'], result['y1'], result['x2'], result['y2'], result['x3'], result['y3'], result['x4'], result['y4']);
        obj.save('test.png');
    } catch (error) {
        console.log(error);
    }
})();

Node.js command line document rectification

Web

  1. Install express, multer and sharp:

     npm install express multer sharp
    
    • express is a web framework for Node.js.
    • multer is a middleware for handling multipart/form-data.
    • sharp is a high performance Node.js image processing library.
  2. Create an app.js file and add the following code to initiate a web server:

     const express = require('express');
     const multer = require('multer');
     const sharp = require('sharp');
     const app = express();
     const http = require('http');
     const server = http.createServer(app);
     const bodyParser = require('body-parser');
     const upload = multer({ dest: 'uploads/' });
     const DocRectifier = require('docrectifier4nodejs');
     DocRectifier.initLicense('LICENSE-KEY');
     var obj = new DocRectifier();
     obj.setParameters(DocRectifier.Template.color);
        
     app.use(express.static('public'));
     app.use('/node_modules', express.static(__dirname + '/node_modules'));
     const port = process.env.PORT || 3000;
        
     server.listen(port, '0.0.0.0', () => {
         host = server.address().address;
         console.log(`Server running at http://0.0.0.0:${port}/`);
     });
    
  3. Include an HTTP POST endpoint in the application for uploading an image file. This endpoint will handle the rectification of the document image and return the rectified image to the client:

     app.post('/rectifydocument', upload.single('image'), async (req, res) => {
         const file = req.file;
        
         if (!file) {
             return res.status(400).send('No file uploaded.');
         }
        
         try {
             console.log('Uploaded file details:', file);
             let results = await obj.detectFileAsync(file.path);
             console.log(results);
             let result = results[0];
             result = await obj.normalizeFileAsync(file.path, result['x1'], result['y1'], result['x2'], result['y2'], result['x3'], result['y3'], result['x4'], result['y4']);
             console.log(result);
        
             let data = result['data']
             let width = result['width']
             let height = result['height']
             for (let i = 0; i < data.length; i += 4) {
                 const red = data[i];
                 const blue = data[i + 2];
                 data[i] = blue;
                 data[i + 2] = red;
             }
             sharp(data, {
                 raw: {
                     width: width,
                     height: height,
                     channels: 4
                 }
             })
                 .jpeg() 
                 .toBuffer() 
                 .then(jpegBuffer => {
                     res.set('Content-Type', 'application/octet-stream');
                     res.send(jpegBuffer);
                 })
                 .catch(err => {
                     console.error(err);
                     res.status(500).send('An error occurred while processing the image.');
                 });
         }
         catch (err) {
             console.error(err);
             return res.status(500).send('An error occurred while processing the image.');
         }
        
     });
        
    
  4. Design a web page and incorporate a button click event that uploads an image file selected through the <input type="file"> element. This will also display the rectified image returned from the server.

     async function rectifyDocument() {
         const input = document.getElementById('document-file');
         const file = input.files[0];
        
         if (!file) {
             return;
         }
        
         const formData = new FormData();
         formData.append('image', file);
        
         let response = await fetch('/rectifydocument', {
             method: 'POST',
             body: formData
         });
        
         if (response.headers.get('Content-Type').includes('text/plain')) {
             let data = await response.text();
             document.getElementById('document-result').innerHTML = data;
        
             let divElement = document.getElementById("document-result");
             divElement.style.display = "block";
        
             divElement = document.getElementById("document-rectified-image");
             divElement.style.display = "none";
         }
         else if (response.headers.get('Content-Type').includes('application/octet-stream')) {
             let data = await response.blob();
             let img = document.getElementById('document-rectified-image');
             let url = URL.createObjectURL(data);
             img.src = url;
        
             let divElement = document.getElementById("document-rectified-image");
             divElement.style.display = "block";
        
             divElement = document.getElementById("document-result");
             divElement.style.display = "none";
         }
     }
    

    Node.js web document rectification

Source Code

https://github.com/yushulx/nodejs-document-rectification