How to Build Java Edge Detection Application to Scan and Normalize Documents
Tags: java, jni, programming, document
Published: 2022-11-23 07:29:44

[Dynamsoft Document Normalizer SDK](https://www.dynamsoft.com/document-normalizer/docs/introduction/?ver=latest) helps developers to quickly build document scanning applications. It provides a set of APIs to detect document edges and normalize document images. Currently, the SDK only supports `C/C++`, `Android`, `iOS`, `Xamarin.Forms` and `JavaScript`. Although there is no Java edition available for download yet, we can make it by ourselves. This article aims to encapsulate the Dynamsoft Document Normalizer C++ libraries into a Java JAR package. The JAR package can be used in Java applications on Windows and Linux.

## Prerequisites
- [OpenJDK](https://openjdk.org/install/)
- [CMake](https://cmake.org/download/)
- [Maven](https://maven.apache.org/download.cgi)
- [Dynamsoft Document Normalizer C/C++ SDK v1.0.10](https://www.dynamsoft.com/document-normalizer/downloads/1000023-confirmation/)

## How to Build Java JNI Project with CMake

First, we start a new Java project and create a `NativeDocumentScanner.java` file that defines some native methods. The native methods are used to load the native libraries and bridge the C++ APIs.

```java
package com.dynamsoft.ddn;

import java.util.ArrayList;

public class NativeDocumentScanner {
	
	private long nativePtr = 0;

	static {
		try {
			if (NativeLoader.load()) {
				System.out.println("Successfully loaded Dynamsoft Document Normalizer.");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public NativeDocumentScanner() {
		nativePtr = nativeCreateInstance();
	}
	
	public void destroyInstance() {
		if (nativePtr != 0)
			nativeDestroyInstance(nativePtr);
	}
	
	public static int setLicense(String license) {
		return nativeInitLicense(license);
	}
	
	public ArrayList<DocumentResult> detectFile(String fileName) {
		return nativeDetectFile(nativePtr, fileName);
	}

	public String getVersion() {
		return nativeGetVersion();
	}

	public NormalizedImage normalizeFile(String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
		return nativeNormalizeFile(nativePtr, fileName, x1, y1, x2, y2, x3, y3, x4, y4);	
	}

	public int setParameters(String parameters) {
		return nativeSetParameters(nativePtr, parameters);
	}

	public int saveImage(NormalizedImage image, String fileName) {
		return nativeSaveImage(image, fileName);
	}

	private native static int nativeInitLicense(String license);
	
	private native long nativeCreateInstance();
	
	private native void nativeDestroyInstance(long nativePtr);
	
	private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);

	private native String nativeGetVersion();

	private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);

	private native int nativeSetParameters(long nativePtr, String parameters);

	private native int nativeSaveImage(NormalizedImage image, String fileName);
}
```

Then, we use the `javah` tool to generate the header file `NativeDocumentScanner.h` for the Java class `NativeDocumentScanner`. 

```bash
cd src/main/java
javah -o ../../../jni/NativeDocumentScanner.h com.dynamsoft.ddn.NativeDocumentScanner
```

The tool eliminates the need to write the header file manually. 

```c++
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_dynamsoft_ddn_NativeDocumentScanner */

#ifndef _Included_com_dynamsoft_ddn_NativeDocumentScanner
#define _Included_com_dynamsoft_ddn_NativeDocumentScanner
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeInitLicense
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeInitLicense
  (JNIEnv *, jclass, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeCreateInstance
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeCreateInstance
  (JNIEnv *, jobject);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeDestroyInstance
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDestroyInstance
  (JNIEnv *, jobject, jlong);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeDetectFile
 * Signature: (JLjava/lang/String;)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDetectFile
  (JNIEnv *, jobject, jlong, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeGetVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeGetVersion
  (JNIEnv *, jobject);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeNormalizeFile
 * Signature: (JLjava/lang/String;IIIIIIII)Lcom/dynamsoft/ddn/NormalizedImage;
 */
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeNormalizeFile
  (JNIEnv *, jobject, jlong, jstring, jint, jint, jint, jint, jint, jint, jint, jint);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeSetParameters
 * Signature: (JLjava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSetParameters
  (JNIEnv *, jobject, jlong, jstring);

/*
 * Class:     com_dynamsoft_ddn_NativeDocumentScanner
 * Method:    nativeSaveImage
 * Signature: (Lcom/dynamsoft/ddn/NormalizedImage;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSaveImage
  (JNIEnv *, jobject, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

```

Create a `NativeDocumentScanner.cxx` file to implement the native methods.  We will talk about the implementation in the later section.

Now, open the `CMakeLists.txt` file and add the following build configurations:

```cmake
cmake_minimum_required (VERSION 2.6)
project (ddn)
MESSAGE( STATUS "PROJECT_NAME: " ${PROJECT_NAME} )

find_package(JNI REQUIRED)
include_directories(${JNI_INCLUDE_DIRS})

MESSAGE( STATUS "JAVA_INCLUDE: " ${JAVA_INCLUDE})

# Check lib
if (CMAKE_HOST_WIN32)
    set(WINDOWS 1)
elseif(CMAKE_HOST_APPLE)
    set(MACOS 1)
elseif(CMAKE_HOST_UNIX)
    set(LINUX 1)
endif()

# Set RPATH
if(CMAKE_HOST_UNIX)
    SET(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wl,-rpath=$ORIGIN")
    SET(CMAKE_INSTALL_RPATH "$ORIGIN")
    SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()

# Add search path for include and lib files
if(WINDOWS)
    link_directories("${PROJECT_SOURCE_DIR}/lib/win/" ${JNI_LIBRARIES}) 
elseif(LINUX)
    link_directories("${PROJECT_SOURCE_DIR}/lib/linux/" ${JNI_LIBRARIES})
endif()
include_directories("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/include/")


# Add the library
add_library(ddn SHARED NativeDocumentScanner.cxx)
if(WINDOWS)
    target_link_libraries (${PROJECT_NAME} "DynamsoftCorex64" "DynamsoftDocumentNormalizerx64")
else()
    target_link_libraries (${PROJECT_NAME} "DynamsoftCore" "DynamsoftDocumentNormalizer" pthread)
endif()

# Set installation directory
set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/../src/main/")
set(LIBRARY_PATH "java/com/dynamsoft/ddn/native")
if(WINDOWS)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/lib/win/" DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/win")
    install (TARGETS ddn DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/win")
elseif(LINUX)
    install (DIRECTORY "${PROJECT_SOURCE_DIR}/lib/linux/" DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/linux")
    install (TARGETS ddn DESTINATION "${CMAKE_INSTALL_PREFIX}${LIBRARY_PATH}/linux")
endif()
```

- `find_package(JNI REQUIRED)`: Find the JNI library in the system.
- `include_directories`: Add the include path for the JNI library and Dynamsoft Document Normalizer SDK. 
- `link_directories`: Add the search path for the C++ library files.  
- `add_library`: Build a shared library.  
- `target_link_libraries`: Link the library with the Dynamsoft Document Normalizer SDK.
- `install`: Copy the library files to the target folder.


Run the following commands to build the JNI project on Windows and Linux:

```bash
# Windows
mkdir build
cd build
cmake -DCMAKE_GENERATOR_PLATFORM=x64 ..
cmake --build . --config Release --target install

# Linux
mkdir build
cd build
cmake .. 
cmake --build . --config Release --target install
```

It will generate a `dnn.dll` for Windows and a `libdnn.so` for Linux. In the next section, we will package the native libraries into a JAR file.

## How to Build Java JAR Package with C++ Libraries Using Maven
Create a `pom.xml` file, in which we define the resource path where native library files are located and use the `Maven Assembly Plugin` to package the native libraries into a JAR file. 

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.dynamsoft</groupId>
	<artifactId>ddn</artifactId>
	<version>1.0.0</version>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<excludes>
					<exclude>**/*.md</exclude>
					<exclude>**/*.h</exclude>
					<exclude>**/*.lib</exclude>
					<exclude>**/*.java</exclude>
				</excludes>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.5.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
```

After that, run the following command to generate `ddn-1.0.0.jar`:

```bash
mvn package
```

## How to Load Shared Libraries from JAR Package
The JAR file contains class files and platform-specific libraries. To call these native libraries in Java applications, we need to firstly extract them to a temporary folder and then call `System.load()` to load the native library. 

```java
private static boolean extractResourceFiles(String ddnNativeLibraryPath, String ddnNativeLibraryName,
			String tempFolder) throws IOException {
  String[] filenames = null;
  if (Utils.isWindows()) {
    filenames = new String[] {"api-ms-win-core-file-l1-2-0.dll",
    "api-ms-win-core-file-l2-1-0.dll",
    "api-ms-win-core-localization-l1-2-0.dll",
    "api-ms-win-core-processthreads-l1-1-1.dll",
    "api-ms-win-core-synch-l1-2-0.dll",
    "api-ms-win-core-timezone-l1-1-0.dll",
    "api-ms-win-crt-conio-l1-1-0.dll",
    "api-ms-win-crt-convert-l1-1-0.dll",
    "api-ms-win-crt-environment-l1-1-0.dll",
    "api-ms-win-crt-filesystem-l1-1-0.dll",
    "api-ms-win-crt-heap-l1-1-0.dll",
    "api-ms-win-crt-locale-l1-1-0.dll",
    "api-ms-win-crt-math-l1-1-0.dll",
    "api-ms-win-crt-multibyte-l1-1-0.dll",
    "api-ms-win-crt-runtime-l1-1-0.dll",
    "api-ms-win-crt-stdio-l1-1-0.dll",
    "api-ms-win-crt-string-l1-1-0.dll",
    "api-ms-win-crt-time-l1-1-0.dll",
    "api-ms-win-crt-utility-l1-1-0.dll",
    "concrt140.dll",
    "DynamicImagex64.dll",
    "DynamicPdfCorex64.dll",
    "DynamicPdfx64.dll",
    "DynamsoftCorex64.dll",
    "DynamsoftImageProcessingx64.dll",
    "DynamsoftIntermediateResultx64.dll",
    "msvcp140.dll",
    "msvcp140_1.dll",
    "msvcp140_2.dll",
    "ucrtbase.dll",
    "vccorlib140.dll",
    "vcomp140.dll",
    "vcruntime140.dll", "DynamsoftDocumentNormalizerx64.dll", "ddn.dll"};
  }
  else if (Utils.isLinux()) {
    filenames = new String[] {"libddn.so", "libDynamicImage.so", "libDynamicPdf.so", "libDynamicPdfCore.so", "libDynamsoftCore.so", "libDynamsoftDocumentNormalizer.so", "libDynamsoftImageProcessing.so", "libDynamsoftIntermediateResult.so"};
  }
  
  boolean ret = true;
  
  for (String file : filenames) {
    ret &= extractAndLoadLibraryFile(ddnNativeLibraryPath, file, tempFolder);
  }
  
  return ret;
}

private static boolean extractAndLoadLibraryFile(String libFolderForCurrentOS, String libraryFileName,
			String targetFolder) {
  String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName;

  String extractedLibFileName = libraryFileName;
  File extractedLibFile = new File(targetFolder, extractedLibFileName);

  try {
    if (extractedLibFile.exists()) {
      // test md5sum value
      String md5sum1 = md5sum(NativeDocumentScanner.class.getResourceAsStream(nativeLibraryFilePath));
      String md5sum2 = md5sum(new FileInputStream(extractedLibFile));

      if (md5sum1.equals(md5sum2)) {
        return loadNativeLibrary(targetFolder, extractedLibFileName);
      } else {
        // remove old native library file
        boolean deletionSucceeded = extractedLibFile.delete();
        if (!deletionSucceeded) {
          throw new IOException(
              "failed to remove existing native library file: " + extractedLibFile.getAbsolutePath());
        }
      }
    }

    // Extract file into the current directory
    InputStream reader = NativeDocumentScanner.class.getResourceAsStream(nativeLibraryFilePath);
    FileOutputStream writer = new FileOutputStream(extractedLibFile);
    byte[] buffer = new byte[1024];
    int bytesRead = 0;
    while ((bytesRead = reader.read(buffer)) != -1) {
      writer.write(buffer, 0, bytesRead);
    }

    writer.close();
    reader.close();

    if (!System.getProperty("os.name").contains("Windows")) {
      try {
        Runtime.getRuntime().exec(new String[] { "chmod", "755", extractedLibFile.getAbsolutePath() })
            .waitFor();
      } catch (Throwable e) {
      }
    }

    return loadNativeLibrary(targetFolder, extractedLibFileName);
  } catch (IOException e) {
    System.err.println(e.getMessage());
    return false;
  }

}

private static synchronized boolean loadNativeLibrary(String path, String name) {
  File libPath = new File(path, name);
  if (libPath.exists()) {
    try {
      System.load(new File(path, name).getAbsolutePath());
      return true;
    } catch (UnsatisfiedLinkError e) {
      System.err.println(e);
      return false;
    }

  } else
    return false;
}
```

Note: when loading DLL files on Windows, the sequence of loading DLL files is important. The DLL files that are loaded first should not depend on other DLL files. For example, `ddn.dll` depends on `DynamsoftDocumentNormalizerx64.dll`, so `DynamsoftDocumentNormalizerx64.dll` should be loaded first. You will see `unsatisfiedLinkError` if you make the loading sequence wrong.

## How to Implement JNI APIs for Document Edge Detection and Normalization
In this section, you will see how to implement APIs in Java and C++ to do document edge detection, perspective correction and image enhancement.

### Initialize the Dynamsoft Document Normalizer
Since the license works globally, we create a static method to set the license. The method only needs to be called once. 

```java
private native static int nativeInitLicense(String license);
```

```cpp
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeInitLicense(JNIEnv *env, jclass, jstring license)
{
  const char *pszLicense = env->GetStringUTFChars(license, NULL);
  char errorMsgBuffer[512];
  // Click https://www.dynamsoft.com/customer/license/trialLicense/?product=ddn to get a trial license.
  int ret = DC_InitLicense(pszLicense, errorMsgBuffer, 512);
  printf("DC_InitLicense: %s\n", errorMsgBuffer);
  env->ReleaseStringUTFChars(license, pszLicense);
  return ret;
}
```

The `GetStringUTFChars` method is used to convert Java string to C string. Don't forget release the memory after using it.


### Create and Destroy Document Scanner Instance
In C/C++, we use `DDN_CreateInstance` to create a document scanner instance and use `DDN_DestroyInstance` to destroy the instance. 
```cpp
JNIEXPORT jlong JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeCreateInstance(JNIEnv *, jobject)
{
  return (jlong)DDN_CreateInstance();
}

JNIEXPORT void JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDestroyInstance(JNIEnv *, jobject, jlong handler)
{
  if (handler)
  {
    DDN_DestroyInstance((void *)handler);
  }
}
```

When creating a document scanner instance, the `nativeCreateInstance()` method is called in constructor of `DocumentScanner` class. The native pointer is saved in Java.

```java
private long nativePtr = 0;

public NativeDocumentScanner() {
  nativePtr = nativeCreateInstance();
}
```

The C++ object is kept in memory until the `destroyInstance()` method is called.
```java
public void destroyInstance() {
  if (nativePtr != 0)
    nativeDestroyInstance(nativePtr);
}
```

### Configure Parameters for Document Normalizer
The parameter configuration allows you to change the behavior of the document scanner. 


```java
private native int nativeSetParameters(long nativePtr, String parameters);
```

```cpp
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSetParameters(JNIEnv *env, jobject, jlong ptr, jstring parameters)
{
  if (ptr)
  {
    void *handler = (void *)ptr;
    const char *params = env->GetStringUTFChars(parameters, NULL);
    char errorMsgBuffer[512];
    int ret = DDN_InitRuntimeSettingsFromString(handler, params, errorMsgBuffer, 512);
    printf("Init runtime settings: %s\n", errorMsgBuffer);

    env->ReleaseStringUTFChars(parameters, params);
    return ret;
  }
  
  return -1;
}
```

For example, you can change the normalized image color mode:

```java
public final static String binary = "{\"GlobalParameter\":{\"Name\":\"GP\"},\"ImageParameterArray\":[{\"Name\":\"IP-1\",\"NormalizerParameterName\":\"NP-1\"}],\"NormalizerParameterArray\":[{\"Name\":\"NP-1\",\"ColourMode\": \"ICM_BINARY\" }]}";

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

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


### Document Edge Detection
The `DDN_DetectQuadFromFile()` method is used to detect the document edge from an image file. 

```java
private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);
```

```cpp
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeDetectFile(JNIEnv *env, jobject, jlong ptr, jstring fileName)
{
  jobject arrayList = NULL;
  if (ptr)
  {
    jclass documentResultClass = env->FindClass("com/dynamsoft/ddn/DocumentResult");
    if (NULL == documentResultClass)
      printf("FindClass failed\n");

    jmethodID documentResultConstructor = env->GetMethodID(documentResultClass, "<init>", "(IIIIIIIII)V");
    if (NULL == documentResultConstructor)
      printf("GetMethodID failed\n");

    jclass arrayListClass = env->FindClass("java/util/ArrayList");
    if (NULL == arrayListClass)
      printf("FindClass failed\n");

    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
    if (NULL == arrayListConstructor)
      printf("GetMethodID failed\n");

    jmethodID arrayListAdd = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
    if (NULL == arrayListAdd)
      printf("GetMethodID failed\n");

    void *handler = (void *)ptr;
    const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

    DetectedQuadResultArray *pResults = NULL;
  
    int ret = DDN_DetectQuadFromFile(handler, pszFileName, "", &pResults);
    if (ret)
    {
      printf("Detection error: %s\n", DC_GetErrorString(ret));
    }

    if (pResults)
    {
      int count = pResults->resultsCount;
      arrayList = env->NewObject(arrayListClass, arrayListConstructor);
    
      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];
        
        jobject object = env->NewObject(documentResultClass, documentResultConstructor, confidence, x1, y1, x2, y2, x3, y3, x4, y4);

        env->CallBooleanMethod(arrayList, arrayListAdd, object);
      }
    }

    if (pResults != NULL)
          DDN_FreeDetectedQuadResultArray(&pResults);

    env->ReleaseStringUTFChars(fileName, pszFileName);
  }

  return arrayList;
}
```

We need to create a `DocumentResult` class to store the detection result.

```java
public class DocumentResult {
    public int confidence;
    public int x1, y1, x2, y2, x3, y3, x4, y4;

    public DocumentResult(int confidence, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
        this.confidence = confidence;
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x3 = x3;
        this.y3 = y3;
        this.x4 = x4;
        this.y4 = y4;
    }
}
```

### Document Normalization

After getting the quadrilateral coordinates of the document, we call `DDN_NormalizeFile()` to normalize the document.

```java
private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);
```

```cpp
JNIEXPORT jobject JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeNormalizeFile(JNIEnv *env, jobject, jlong ptr, jstring fileName, jint x1, jint y1, jint x2, jint y2, jint x3, jint y3, jint x4, jint y4)
{
  if (ptr)
  {

    jclass normalizedImageClass = env->FindClass("com/dynamsoft/ddn/NormalizedImage");
    if (NULL == normalizedImageClass)
      printf("FindClass failed\n");

    jmethodID normalizedImageConstructor = env->GetMethodID(normalizedImageClass, "<init>", "(IIII[BII)V");
    if (NULL == normalizedImageConstructor)
      printf("GetMethodID failed\n");

    const char *pszFileName = env->GetStringUTFChars(fileName, NULL);
    
    void *handler = (void *)ptr;

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

    NormalizedImageResult* normalizedResult = NULL;

    int errorCode = DDN_NormalizeFile(handler, pszFileName, "", &quad, &normalizedResult);
    if (errorCode != DM_OK)
      printf("%s\r\n", DC_GetErrorString(errorCode));

    ImageData *imageData = normalizedResult->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;

    jbyteArray byteArray = env->NewByteArray(length);
    env->SetByteArrayRegion(byteArray, 0, length, (jbyte *)data);
    jobject object = env->NewObject(normalizedImageClass, normalizedImageConstructor, width, height, stride, format, byteArray, orientation, length);
    env->ReleaseStringUTFChars(fileName, pszFileName);

    if (normalizedResult != NULL)
      DDN_FreeNormalizedImageResult(&normalizedResult);

    return object;
  }
  return NULL;
}   
```

The normalized image data are stored in `NormalizedImage` class.

```java
public class NormalizedImage {
    public int width;
    public int height;
    public int stride;
    public int format;
    public byte[] data;
    public int orientation;
    public int length;

    public NormalizedImage(int width, int height, int stride, int format, byte[] data, int orientation, int length) {
        this.width = width;
        this.height = height;
        this.stride = stride;
        this.format = format;
        this.data = data;
        this.orientation = orientation;
        this.length = length;
    }
}
```

### Save the Normalized Document
The `NormalizedImage` class contains image data and image format. So we can convert `NormalizedImage` to `BufferedImage` and use `ImageIO` to save the image data to a file.

```java
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

public void saveImage(String formatName, String fileName) {
    BufferedImage image = null;
    byte[] imageData = null;
    int[] pixels = new int[width * height];
    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    
    if (format == ImagePixelFormat.IPF_RGB_888) {
        
        imageData = data;

        for (int i = 0; i < width * height; i++) {
            int r = imageData[i * 3] & 0xFF;
            int g = imageData[i * 3 + 1] & 0xFF;
            int b = imageData[i * 3 + 2] & 0xFF;
            pixels[i] = (r << 16) | (g << 8) | b;
        }
    }
    else if (format == ImagePixelFormat.IPF_GRAYSCALED) {
        imageData = data;

        for (int i = 0; i < width * height; i++) {
            int gray = imageData[i] & 0xFF;
            pixels[i] = (gray << 16) | (gray << 8) | gray;
        }
    }
    else if (format == ImagePixelFormat.IPF_BINARY) {
        imageData = binary2Grayscale();

        for (int i = 0; i < width * height; i++) {
            int gray = imageData[i] & 0xFF;
            pixels[i] = (gray << 16) | (gray << 8) | gray;
        }
    }
    
    image.setRGB(0, 0, width, height, pixels, 0, width);
    Utils.display(image, "Normalized Image");
    try {
        ImageIO.write(image, formatName, new java.io.File(fileName));
    } catch (Exception e) {
        e.printStackTrace();
    }
}
```

The `ImageIO` class does not support `PDF`, but Dynamsoft Document Normalizer does. The native method `NormalizedImageResult_SaveToFile()` can save a normalized document as BMP, PNG, JPEG and PDF files. The code is a little bit complicated that we need to construct a C++ `ImageData` type from Java `NormalizedImage` object.

```java
private native int nativeSaveImage(NormalizedImage image, String fileName);
```

```cpp
JNIEXPORT jint JNICALL Java_com_dynamsoft_ddn_NativeDocumentScanner_nativeSaveImage(JNIEnv *env, jobject, jobject obj, jstring fileName)
{
  jclass normalizedImageClass = env->FindClass("com/dynamsoft/ddn/NormalizedImage");
  if (NULL == normalizedImageClass)
    printf("FindClass failed\n");

  jfieldID fid = env->GetFieldID(normalizedImageClass, "width", "I");
  if (NULL == fid)
    printf("Get width failed\n");

  jint width = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "height", "I");
  if (NULL == fid)
    printf("Ge height failed\n");

  jint height = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "stride", "I");
  if (NULL == fid)
    printf("Get stride failed\n");

  jint stride = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "format", "I");
  if (NULL == fid)
    printf("Get format failed\n");

  jint format = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "data", "[B");
  if (NULL == fid)
    printf("Get data failed\n");

  jbyteArray byteArray = (jbyteArray)env->GetObjectField(obj, fid);
  jbyte *bytes = env->GetByteArrayElements(byteArray, NULL);

  fid = env->GetFieldID(normalizedImageClass, "orientation", "I");
  if (NULL == fid)
    printf("Get orientation failed\n");

  jint orientation = env->GetIntField(obj, fid);

  fid = env->GetFieldID(normalizedImageClass, "length", "I");
  if (NULL == fid)
    printf("Get length failed\n");

  jint length = env->GetIntField(obj, fid);

  ImageData data;
  data.bytes = (unsigned char *)bytes;
  data.width = width;
  data.height = height;
  data.stride = stride;
  data.format = (ImagePixelFormat)format;
  data.orientation = orientation;
  data.bytesLength = length;

  const char *pszFileName = env->GetStringUTFChars(fileName, NULL);

  NormalizedImageResult normalizedResult;
  normalizedResult.image = &data;
  int ret = NormalizedImageResult_SaveToFile(&normalizedResult, pszFileName);
  if (ret != DM_OK)
    printf("NormalizedImageResult_SaveToFile: %s\r\n", DC_GetErrorString(ret));

  env->ReleaseStringUTFChars(fileName, pszFileName);
  env->ReleaseByteArrayElements(byteArray, bytes, 0);

  return ret;
}
```

In addition to saving images to files, we can also display them on the screen using `JFrame`:

```java
public static void display(BufferedImage image, String title) {
  JFrame frame = new JFrame();
  frame.getContentPane().setLayout(new FlowLayout());
  frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
  frame.setTitle(title);
  frame.getContentPane().add(new JLabel(new ImageIcon(image)));
  frame.pack();
  frame.setVisible(true);
}
```


## How to Build Java Document Scanner Applications on Windows and Linux
1. Get a [30-day free trial license](https://www.dynamsoft.com/customer/license/trialLicense/?product=ddn) and call `setLicense()` to activate the SDK.

    ```java
    int ret = NativeDocumentScanner.setLicense(license);
    ```

2. Create a `NativeDocumentScanner` object:

    ```java
    NativeDocumentScanner scanner = new NativeDocumentScanner();
    ```

3. Set the parameters. By default, the normalized image is a binary image. You can change it to grayscale or color by calling `setParameters()`.

    ```java
    String template = "{\"GlobalParameter\":{\"Name\":\"GP\"},\"ImageParameterArray\":[{\"Name\":\"IP-1\",\"NormalizerParameterName\":\"NP-1\"}],\"NormalizerParameterArray\":[{\"Name\":\"NP-1\",\"ColourMode\": \"ICM_COLOUR\" }]}";
    scanner.setParameters(template);
    ```

4. Detect the document edges:

    ```java
    ArrayList<DocumentResult> results = (ArrayList<DocumentResult>)scanner.detectFile(fileName);
    ```

5. Normalize the document based on the quadrilateral points:

    ```java
    NormalizedImage normalizedImage = scanner.normalizeFile(fileName, result.x1, result.y1, result.x2, result.y2, result.x3, result.y3, result.x4, result.y4);
    ```

6. Save the normalized image as PDF, JPEG or PNG:

    ```java
    scanner.saveImage(normalizedImage, "normalized.pdf");
    ```

## The Whole Steps to Build and Test the Java Document Scanner SDK

```bash
cd jni
mkdir build
cd build
cmake ..
cmake --build . --config Release --target install
cd ../../
mvn package
java -cp target/ddn-1.0.0.jar com.dynamsoft.ddn.Test images/sample-image.png <optional: template.json> <optional: license key>
```

![Java document scanner: document edge detection and normalization](https://www.dynamsoft.com/codepool/img/2022/11/java-document-scanner-edge-detection-normalization.png)


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