slothi-jna

Slothi, a seasoned developer, faces a project where high-speed data processing is crucial.


In his situation the application must talk directly to the hardware. Java alone can’t handle this, as it operates within the JVM’s safe and cozy environment and doesn’t include direct hardware management.


The only option for meeting these requirements comes in the form of third-party native libraries. Slothi must integrate these libraries into the Java ecosystem to meet the project’s requirements.

Java Native Access (JNA)

Slothi discovers Java Native Access (JNA) and it’s like discovering secret superpowers!


JNA allows Java programs to call native code directly, as if they’re simply calling another Java method.


JNA not only simplifies the development process but also opens up new possibilities for using Java in areas where speed and direct system interaction are critical. It’s a perfect tool for those who want to extend the capabilities of their Java applications without getting bogged down by the intricacies of the Native Layer.

How JNA Works

Slothi quickly realizes that Java Native Access (JNA) offers a seamless way to integrate native libraries with Java applications.


JNA acts as a bridge between Java and native libraries, enabling straightforward integration.


The mechanism is designed to be both intuitive and efficient:

  1. Dynamic Linking: JNA allows Java applications to dynamically link with native libraries at runtime. This capability is similar to loading .dll files on Windows or .so files on Linux, but from within Java code.
  2. Direct Mapping: Direct mapping facilitates the immediate connection between Java calls and native functions based on predefined function names and signatures.
  3. Automatic Data Type Conversion: Leveraging a mechanism similar to Java’s reflection, JNA automatically manages the conversion of data types between Java and native code. This feature is essential for handling complex structures and arrays smoothly.
Setting Up JNA in a Java Project

Integrating JNA into a Java project is straightforward, and doing so within a Maven project structure makes it even easier.

Maven Project Structure

Slothi sets up a new Maven project with the directory structure looking like this:

project-name/
|-- pom.xml
|-- src/
    |-- main/
        |-- java/
            |-- (package structure)
        |-- resources/
    |-- test/
        |-- java/
            |-- (test package structure)
        |-- resources/

The src/main/java directory is where Slothi will place his Java code, and src/main/resources is intended for any resources necessary for the application (like configuration files).

Adding JNA

To include JNA in the project, Slothi adds the following dependency to the pom.xml file:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.8.0</version> <!-- Check for the latest version on Maven Central -->
</dependency>

This ensures that JNA is automatically downloaded and added to the project’s classpath.

Loading a Library Using JNA

With the dependency set up, Slothi can now start using JNA to load native libraries.


Here’s how to load a standard C library across different operating systems:


Loading libc on Linux

import com.sun.jna.Native;
import com.sun.jna.Library;

interface CLibrary extends Library {
    CLibrary INSTANCE = Native.load("c", CLibrary.class);
    void printf(String format, Object... args);
}

public class Main {
    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, %s!\n", "World Linux");
    }
}

Loading msvcrt on Windows

import com.sun.jna.Native;
import com.sun.jna.Library;

interface CLibrary extends Library {
    CLibrary INSTANCE = Native.load("msvcrt", CLibrary.class);
    void printf(String format, Object... args);
}

public class Main {
    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, %s!\n", "Windows World");
    }
}

Loading libc on macOS

import com.sun.jna.Native;
import com.sun.jna.Library;

interface CLibrary extends Library {
    CLibrary INSTANCE = Native.load("System", CLibrary.class);
    void printf(String format, Object... args);
}

public class Main {
    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, %s!\n", "macOS World");
    }
}

In each example, Slothi uses JNA to load the respective C standard library and call the printf function, demonstrating basic interaction with native code.


On macOS, System is used to load the system library which includes the standard C functions.


This setup not only helps Slothi understand how to integrate and utilize JNA in his projects but also showcases the ease with which cross-platform native interactions can be achieved using JNA.


Java JNA Example Source Code

Real-World Use Case

Note: This example is a simplified version of what was actually created.

This section details how Slothi utilizes JNA to interact with native layer to meet his task requirements.


Slothi is assigned the task of developing a service that extracts text from images. To achieve this, he needs to interact with system-level libraries, specifically using OCR (Optical Character Recognition) technology.


Slothi decides to use the Tesseract OCR engine due to its robustness and wide adoption in the industry. Tesseract allows for accurate text extraction from various image formats.


His work starts by setting up a new Maven project with the directory structure looking like this:

project-name/
|-- pom.xml
|-- src/
    |-- main/
        |-- java/
            |-- (package structure)
        |-- resources/
            |-- libs/
            |-- tessdata/
            |-- testocr.png


  • project-name/ - The root directory of the project, named after the project itself. It contains the Maven build configuration file and all source code and resources.
  • pom.xml - Maven’s Project Object Model file that defines project dependencies and build configurations.
  • main/java/ - Stores the Java source files arranged according to the package structure
  • main/resources/ - Holds all non-code assets required by the project:
    • libs/ - Contains native libraries required for interfacing with system-level APIs via JNA.
    • tessdata/ - Includes data files for Tesseract , necessary for OCR processing.
    • testocr.png - An image file used for testing OCR functionality.

Managing Native Dependencies

Slothi needs to download the native libraries for Tesseract and Leptonica to enable OCR functionality.


He places these libraries in the libs folder within the resources directory of his project. This organization ensures that these dependencies are easily accessible to the Java application at runtime through JNA.


To ensure that these native libraries are correctly packaged and available during runtime, Slothi updates the pom.xml file of his Maven project to include all files within the resources directory.


This is achieved by specifying resource handling in the build configuration as follows:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
                <includes>
                <include>**/*</include> <!-- Includes all files within the resources directory -->
            </includes>
        </resource>
    </resources>
</build>

Adding Tessdata

For Tesseract to perform OCR effectively, it requires access to language and configuration files stored in the tessdata directory. Slothi ensures that these necessary files are included in his project and correctly referenced by Tesseract during runtime.


Slothi places the tessdata directory containing the language data files required by Tesseract within the resources directory of his Maven project. The files are bundled into the final build thanks to his pom.xml configuration which included all resources.

Adding a Test Image

Slothi includes a test image named testocr.png, to the resources folder within his project. This image is used to validate that the OCR process works as expected, confirming the accuracy and reliability of the text extraction.

Integrating Tesseract with Java

Slothi starts his implementation by setting up an interface to interact with the Tesseract library using Java Native Access (JNA). This interface will allow his Java application to call native methods in the Tesseract OCR library directly.

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public interface Tesseract extends Library {
    Tesseract INSTANCE = Native.load("libtesseract.dylib", Tesseract.class);

    Pointer TessBaseAPICreate();
    int TessBaseAPIInit3(Pointer handle, String dataPath, String language);
    void TessBaseAPISetImage2(Pointer handle, Pointer pix);
    Pointer TessBaseAPIGetUTF8Text(Pointer handle);
    void TessBaseAPIEnd(Pointer handle);
    void TessBaseAPIDelete(Pointer handle);
    void TessDeleteText(Pointer text);
}
  • TessBaseAPICreate creates a new instance of the Tesseract API, preparing it for initialization and use.
  • TessBaseAPIInit3 initializes the created Tesseract instance with specified data directories and language settings, configuring it for OCR operations.
  • TessBaseAPISetImage2 assigns an image to the Tesseract instance, which is then processed to detect and interpret text.
  • TessBaseAPIGetUTF8Text retrieves the text recognized in the image, returning it as a UTF-8 encoded string.
  • TessBaseAPIEnd and TessBaseAPIDelete are cleanup methods that end the OCR session and delete the Tesseract instance, respectively, freeing up all associated resources and memory.
  • TessDeleteText specifically frees the memory allocated for the text extracted by Tesseract.


Through these methods, the class facilitates direct and efficient management of OCR tasks, from initialization and image processing to text extraction and resource cleanup, directly within a Java environment.


In Slothi’s journey to harness OCR capabilities, alongside Tesseract, he incorporates the Leptonica library, which is crucial for image preprocessing.


The Leptonica class in Java serves as a gateway to the native functionalities of the Leptonica library, facilitating image handling operations that are essential before text extraction can occur.

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public interface Leptonica extends Library {
    Leptonica INSTANCE = Native.load("libleptonica.dylib", Leptonica.class);
    Pointer pixRead(String filename);
}


  • pixRead(): The core functionality of Leptonica is embodied in the pixRead method, which loads an image from a given filepath into memory as a Pix structure. This capability is crucial for the initial stages of OCR processing, as it handles the conversion of image files into a format that can be manipulated and analyzed by Tesseract for text extraction.


In Slothi’s quest to seamlessly integrate native libraries into his Java application, the LibraryLoader class emerges as a cornerstone for handling these essential resources.


This class is meticulously crafted to ensure that native libraries, such as those required by JNA for OCR operations, are dynamically loaded and managed within the Java runtime environment.

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class LibraryLoader {

    private static final Path tempDir = createTempDirectory();

    private static Path createTempDirectory() {
        try {
            // Create a single temporary directory for all libraries
            return Files.createTempDirectory("nativeLibs");
        } catch (Exception e) {
            throw new RuntimeException("Could not create temp directory for native libraries", e);
        }
    }

    public static void load(String... libraryPaths) throws IOException {
        for (String resourcePath : libraryPaths) {
            InputStream inputStream = LibraryLoader.class.getResourceAsStream("/libs/" + resourcePath);
            if (inputStream == null) {
                throw new IOException("Resource not found: " + resourcePath);
            }

            String fileName = new File(resourcePath).getName();
            File tempFile = new File(tempDir.toFile(), fileName);
            tempFile.deleteOnExit();

            try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
        }
        System.setProperty("jna.library.path", tempDir.toString());
    }
}
  • createTempDirectory: Initializes a dedicated directory for storing native libraries. This isolated environment ensures that native dependencies do not interfere with each other and are easily managed. This method is crucial for maintaining order and accessibility, especially when dealing with multiple native libraries.
  • load: This method is meticulously designed to handle multiple libraries. It iterates through provided paths, extracting libraries from the resource stream and storing them in the temporary directory. This process ensures that even if the resources are embedded within a JAR, they are made available to the application at runtime. This feature is essential for applications that need to ensure all native components are loaded correctly and efficiently.
  • Post-loading Configuration: After loading the libraries, LibraryLoader sets the jna.library.path system property to the path of the temporary directory. This configuration is critical as it enables Java Native Access (JNA) to locate and utilize these libraries seamlessly, thus ensuring that the native integrations function correctly across different environments and platforms


In Slothi’s project, handling resources efficiently is crucial, especially when dealing with OCR capabilities that require specific data files.


The ResourceExtractor class is designed to facilitate the extraction and management of these resources, ensuring they are accessible at runtime.

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ResourceExtractor {
    public static File extract(String resourcePath) throws IOException {
        InputStream resourceStream = getResourceAsStream(resourcePath);
        
        if (resourceStream == null) {
            throw new IOException("Resource not found: " + resourcePath);
        }

        File tempFile = Files.createTempFile("resource-", ".tmp").toFile();
        try (FileOutputStream out = new FileOutputStream(tempFile)) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = resourceStream.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
        return tempFile;
    }

    public static File extractTessdata(String tessdataResourcePath) throws IOException {
        Path tempDir = Files.createTempDirectory("tessdata");
        InputStream source =getResourceAsStream(tessdataResourcePath + "/eng.traineddata");
        Path destination = tempDir.resolve("eng.traineddata");
        Files.copy(source, destination);
        return tempDir.toFile();
    }

    private static InputStream getResourceAsStream(String resourcePath) {
        return ResourceExtractor.class.getResourceAsStream(resourcePath);
    }
}
  • extract: Retrieves and temporarily stores resources from the application’s classpath into the file system. This operation is vital for accessing resources that are embedded within JAR files, ensuring they are available for further processing or execution. This method is essential in environments where resources need to be dynamically managed and accessed at runtime.
  • extractTessdata: Specifically designed for extracting Tesseract’s training data files (tessdata) to a temporary directory. This method is critical for ensuring that OCR data files are available for Tesseract’s initialization and operation, thus enabling accurate and effective text recognition processes.
  • getResourceAsStream: Fetches resources as an input stream from the classpath, facilitating the extraction process by other methods. This foundational method is key for providing the data flow necessary for the effective management of embedded resources, supporting the functionality of the entire resource extraction process.


Slothi’s OCRProcessor class is the centerpiece of his OCR application, designed to harness the power of Tesseract and Leptonica for text extraction from images. This class encapsulates the entire process from loading native libraries to processing images and extracting text.


Upon the class’s loading, OCRProcessor initiates by loading the required native libraries for Tesseract and Leptonica using the LibraryLoader class.


This step is critical to ensure all necessary components are in place before any OCR operations can begin.

import com.sun.jna.Pointer;
import java.io.File;

public class OCRProcessor {
    static {
        try {
            LibraryLoader.load("libtesseract.dylib", "libleptonica.dylib");
        } catch (Exception e) {
            throw new RuntimeException("Failed to load native libraries", e);
        }
    }

    public static void main(String[] args) {
        File imageFile;
        File tessdataDir;
        try {
            imageFile = ResourceExtractor.extract("/testocr.png");
            tessdataDir = ResourceExtractor.extractTessdata("/tessdata");
        } catch (Exception e) {
            System.err.println("Failed to resources: " + e.getMessage());
            e.printStackTrace();
            return;
        }

        Pointer tesseract = Tesseract.INSTANCE.TessBaseAPICreate();
        Pointer image = null;
        try {
            // Initialize Tesseract API
            if (Tesseract.INSTANCE.TessBaseAPIInit3(tesseract, tessdataDir.getAbsolutePath(), "eng") != 0) {
                throw new RuntimeException("Could not initialize Tesseract.");
            }

            // Load the image using Leptonica
            image = Leptonica.INSTANCE.pixRead(imageFile.getAbsolutePath());
            if (image == null) {
                throw new RuntimeException("Failed to read the image file.");
            }

            // Set the image to Tesseract
            Tesseract.INSTANCE.TessBaseAPISetImage2(tesseract, image);

            // Extract text
            Pointer textPointer = Tesseract.INSTANCE.TessBaseAPIGetUTF8Text(tesseract);
            if (textPointer != null) {
                String extractedText = textPointer.getString(0);
                System.out.println("Extracted Text: " + extractedText);
                Tesseract.INSTANCE.TessDeleteText(textPointer); // Free text memory
            }

        } catch (Exception e) {
            System.err.println(e.getMessage());
        } finally {
            Tesseract.INSTANCE.TessBaseAPIEnd(tesseract);
            Tesseract.INSTANCE.TessBaseAPIDelete(tesseract);
            if (imageFile != null && imageFile.exists()) {
                imageFile.delete();
            }
            if (tessdataDir != null && tessdataDir.exists()) {
                tessdataDir.delete();
            }
        }
    }
}
  • Resource Extraction: It begins by extracting the necessary resources, including the OCR test image and Tesseract training data (tessdata), using the ResourceExtractor class.
  • Tesseract and Leptonica Integration: The method then creates a Tesseract instance and uses Leptonica to load the image file into memory.
  • OCR Execution: After initializing Tesseract with the appropriate data path and language settings, the image is set for processing. The text recognized from the image is then extracted and output to the console.
  • Resource Management and Cleanup: Following the extraction, the method ensures that all resources are appropriately freed and any temporary files are cleaned up, maintaining system integrity and preventing resource leakage.


Throughout its execution, OCRProcessor meticulously handles exceptions, particularly those related to resource loading and OCR processing failures. This robust error handling is essential to ensure the application remains stable and provides meaningful error messages in case of failures.


The OCRProcessor class is a comprehensive solution designed by Slothi to demonstrate the integration and functionality of optical character recognition within a Java application. It highlights the effective use of JNA to bridge Java with native libraries and showcases the practical application of OCR technology in real-world scenarios.


OCR Processor Source Code