DEV Community 👩‍💻👨‍💻

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build Java Edge Detection Application to Scan and Normalize Documents

Dynamsoft Document Normalizer SDK 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

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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

cd src/main/java
javah -o ../../../jni/NativeDocumentScanner.h com.dynamsoft.ddn.NativeDocumentScanner
Enter fullscreen mode Exit fullscreen mode

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

/* 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

Enter fullscreen mode Exit fullscreen mode

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_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()
Enter fullscreen mode Exit fullscreen mode
  • 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:

# 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
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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

mvn package
Enter fullscreen mode Exit fullscreen mode

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

private native static int nativeInitLicense(String license);
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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

private long nativePtr = 0;

public NativeDocumentScanner() {
  nativePtr = nativeCreateInstance();
}
Enter fullscreen mode Exit fullscreen mode

The C++ object is kept in memory until the destroyInstance() method is called.

public void destroyInstance() {
  if (nativePtr != 0)
    nativeDestroyInstance(nativePtr);
}
Enter fullscreen mode Exit fullscreen mode

Configure Parameters for Document Normalizer

The parameter configuration allows you to change the behavior of the document scanner.

private native int nativeSetParameters(long nativePtr, String parameters);
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

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

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\"}]}";
Enter fullscreen mode Exit fullscreen mode

Document Edge Detection

The DDN_DetectQuadFromFile() method is used to detect the document edge from an image file.

private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

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

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Document Normalization

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

private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);
Enter fullscreen mode Exit fullscreen mode
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;
}   
Enter fullscreen mode Exit fullscreen mode

The normalized image data are stored in NormalizedImage class.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

private native int nativeSaveImage(NormalizedImage image, String fileName);
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

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

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);
}
Enter fullscreen mode Exit fullscreen mode

How to Build Java Document Scanner Applications on Windows and Linux

  1. Get a 30-day free trial license and call setLicense() to activate the SDK.

    int ret = NativeDocumentScanner.setLicense(license);
    
  2. Create a NativeDocumentScanner object:

    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().

    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:

    ArrayList<DocumentResult> results = (ArrayList<DocumentResult>)scanner.detectFile(fileName);
    
  5. Normalize the document based on the quadrilateral points:

    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:

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

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

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>
Enter fullscreen mode Exit fullscreen mode

Java document scanner: document edge detection and normalization

Source Code

https://github.com/yushulx/java-document-scanner-sdk

Top comments (0)

Create an Account!

👀 Just want to lurk?

That's fine, you can still create an account and turn on features like 🌚 dark mode.