How to Build Java Edge Detection Application to Scan and Normalize Documents
Xiao Ling
Posted on November 23, 2022
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);
}
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
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
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()
-
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
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>
After that, run the following command to generate ddn-1.0.0.jar
:
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.
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.
private native static int nativeInitLicense(String license);
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.
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.
private long nativePtr = 0;
public NativeDocumentScanner() {
nativePtr = nativeCreateInstance();
}
The C++ object is kept in memory until the destroyInstance()
method is called.
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.
private native int nativeSetParameters(long nativePtr, String parameters);
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:
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.
private native ArrayList<DocumentResult> nativeDetectFile(long nativePtr, String fileName);
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.
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.
private native NormalizedImage nativeNormalizeFile(long nativePtr, String fileName, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);
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.
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.
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.
private native int nativeSaveImage(NormalizedImage image, String fileName);
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
:
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
-
Get a 30-day free trial license and call
setLicense()
to activate the SDK.
int ret = NativeDocumentScanner.setLicense(license);
-
Create a
NativeDocumentScanner
object:
NativeDocumentScanner scanner = new NativeDocumentScanner();
-
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);
-
Detect the document edges:
ArrayList<DocumentResult> results = (ArrayList<DocumentResult>)scanner.detectFile(fileName);
-
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);
-
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>
Source Code
Posted on November 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.