DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Building a Java Barcode Scanner with Camera SDK, ZXing, and Dynamsoft Barcode Reader

Java Development Kit (JDK) does not provide a built-in API for camera access. A common workaround is to use OpenCV with Java bindings (such as JavaCV). However, OpenCV is a fairly large library, which may not be suitable for all applications.

In this tutorial, we will demonstrate how to build a Java Camera SDK by wrapping the LiteCam C++ SDK, and then integrate it with ZXing and Dynamsoft Barcode Reader to create a complete barcode scanning solution — from setup to deployment.

Demo: Java Barcode Scanner & Reader

Prerequisites

System Requirements

  • Java JDK 8+ and Maven 3.6+
  • A camera device (for scanning)
  • Platform dependencies: Windows (Visual Studio), Linux (libx11-dev libv4l-dev), macOS (Xcode)
  • A 30-day free trial license for Dynamsoft Barcode Reader

Platform-Specific Requirements

Windows

  • Visual Studio 2019 or later (for building from source)
  • Media Foundation (included with Windows)
  • Windows 10/11 recommended

Linux

sudo apt update sudo apt install libx11-dev libv4l-dev 
Enter fullscreen mode Exit fullscreen mode

macOS

  • Xcode development tools (for building from source)
  • AVFoundation framework (included with macOS)

Project Overview

This project consists of two main components:

  1. LiteCam SDK: A lightweight, cross-platform Java camera capture library
  2. Maven Barcode Scanner: A full-featured barcode scanning application with dual detection engines

    Java barcode scanner reader

Key Features

  • Real-time Camera Feed: High-performance camera capture using native JNI
  • Dual Barcode Engines: Switch between ZXing (open-source) and Dynamsoft (enterprise)
  • Visual Overlays: Real-time barcode highlighting with coordinates
  • File Mode Support: Drag-and-drop image processing
  • Cross-Platform: Windows, macOS, and Linux support
  • Convenience Scripts: Cross-platform build and run scripts for easy development

LiteCam SDK Overview

LiteCam is a lightweight C++ camera SDK. A JNI bridge turns it into a Java-compatible library.

Core Features

  • Cross-Platform Video Capture: Uses platform-native APIs (Media Foundation, V4L2, AVFoundation)
  • RGB Frame Access: Direct access to uncompressed RGB data
  • JNI Integration: Optimized native bridge for Java applications
  • Resolution Control: Support for multiple resolutions and frame rates

Architecture

┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ │ Java App │────│ LiteCam │────│ Native Camera │ │ │ │ JNI Bridge │ │ APIs │ └─────────────────┘ └──────────────┘ └─────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Supported Platforms

Platform Camera API Display
Windows Media Foundation GDI/DirectX
Linux Video4Linux (V4L2) X11
macOS AVFoundation Cocoa

Barcode Scanner Application

The Maven Barcode Scanner application demonstrates advanced integration of camera capture with multiple barcode detection engines.

Architecture Overview

┌──────────────────┐ │ Swing GUI │ ├──────────────────┤ │ Camera Panel │ ← Live preview with overlays │ Controls Panel │ ← Engine selection, modes │ Results Panel │ ← Detection history └──────────────────┘ │ ├────────────────────┤ │ Core Engine │ ├────────────────────┤ │ ┌────────────────┐ │ │ │ LiteCam SDK │ │ ← Camera capture │ └────────────────┘ │ │ ┌────────────────┐ │ │ │ ZXing Engine │ │ ← Open-source detection │ └────────────────┘ │ │ ┌────────────────┐ │ │ │ Dynamsoft DBR │ │ ← Enterprise detection │ └────────────────┘ │ └────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Detection Engines Comparison

Feature ZXing Dynamsoft DBR
Cost Free (Apache 2.0) Commercial license
Accuracy Good Excellent
Speed Fast Very Fast
Damaged Codes Limited Advanced
Multi-detection Basic Advanced

Project Structure

The project is organized into two main components:

├── README.md # Project overview and quick start ├── build-jar.ps1/.sh # Scripts to build LiteCam SDK ├── run-litecam.ps1/.sh # Scripts to test LiteCam SDK ├── litecam.jar # Pre-built Camera SDK with natives ├── include/ # C++ headers for camera implementation ├── src/ # C++ camera implementation (cross-platform) ├── java-src/ # Basic LiteCam Java SDK source │ └── com/example/litecam/ │ ├── LiteCam.java # Main camera API │ └── LiteCamViewer.java # Simple camera viewer test └── maven-example/ # Complete Barcode Scanner Application ├── pom.xml # Maven dependencies and build config ├── build.ps1/.sh # Build scripts for barcode scanner ├── run.ps1/.sh # Run scripts for barcode scanner ├── src/main/java/com/example/litecam/ │ └── BarcodeScanner.java # Main barcode scanning application └── target/ # Maven build output └── litecam-barcode-scanner-1.0.0.jar 
Enter fullscreen mode Exit fullscreen mode

Java Camera SDK Development

Step 1: JNI for LiteCam C++ Integration

Create a LiteCamJNI.cpp file to wrap the LiteCam C++ SDK, enabling access from Java:

#include "Camera.h" #include <jni.h> #include <vector> #include <mutex> #include <string>  struct CameraEntry { int id; Camera *cam; }; static std::mutex g_mutex; static std::vector<CameraEntry> g_cameras; static int g_nextId = 1; static Camera *getCamera(int handle) { std::lock_guard<std::mutex> lock(g_mutex); for (auto &e : g_cameras) if (e.id == handle) return e.cam; return nullptr; } static int registerCamera(Camera *c) { std::lock_guard<std::mutex> lock(g_mutex); int id = g_nextId++; g_cameras.push_back({id, c}); return id; } static void unregisterCamera(int handle) { std::lock_guard<std::mutex> lock(g_mutex); for (auto it = g_cameras.begin(); it != g_cameras.end(); ++it) { if (it->id == handle) { delete it->cam; g_cameras.erase(it); return; } } } static jclass findAndGlobalRef(JNIEnv *env, const char *name) { jclass local = env->FindClass(name); return (jclass)env->NewGlobalRef(local); } extern "C" { JNIEXPORT jobjectArray JNICALL Java_com_example_litecam_LiteCam_listDevices(JNIEnv *env, jclass) { auto devices = ListCaptureDevices(); jclass stringClass = env->FindClass("java/lang/String"); jobjectArray arr = env->NewObjectArray((jsize)devices.size(), stringClass, nullptr); for (jsize i = 0; i < (jsize)devices.size(); ++i) { #ifdef _WIN32  char buffer[512]; wcstombs_s(nullptr, buffer, devices[i].friendlyName, sizeof(buffer)); env->SetObjectArrayElement(arr, i, env->NewStringUTF(buffer)); #else  env->SetObjectArrayElement(arr, i, env->NewStringUTF(devices[i].friendlyName)); #endif  } return arr; } JNIEXPORT jint JNICALL Java_com_example_litecam_LiteCam_open(JNIEnv *env, jobject self, jint deviceIndex) { auto cam = new Camera(); if (!cam->Open(deviceIndex)) { delete cam; return 0; } return registerCamera(cam); } JNIEXPORT void JNICALL Java_com_example_litecam_LiteCam_nativeClose(JNIEnv *, jobject, jint handle) { unregisterCamera(handle); } JNIEXPORT jintArray JNICALL Java_com_example_litecam_LiteCam_listSupportedResolutions(JNIEnv *env, jobject, jint handle) { Camera *cam = getCamera(handle); if (!cam) return nullptr; auto mts = cam->ListSupportedMediaTypes(); // Flatten as width,height pairs sequentially. jintArray arr = env->NewIntArray((jsize)(mts.size() * 2)); std::vector<jint> tmp; tmp.reserve(mts.size() * 2); for (auto &m : mts) { tmp.push_back((jint)m.width); tmp.push_back((jint)m.height); } env->SetIntArrayRegion(arr, 0, (jsize)tmp.size(), tmp.data()); return arr; } JNIEXPORT jboolean JNICALL Java_com_example_litecam_LiteCam_setResolution(JNIEnv *, jobject, jint handle, jint w, jint h) { Camera *cam = getCamera(handle); if (!cam) return JNI_FALSE; return cam->SetResolution(w, h) ? JNI_TRUE : JNI_FALSE; } JNIEXPORT jboolean JNICALL Java_com_example_litecam_LiteCam_captureFrame(JNIEnv *env, jobject, jint handle, jobject byteBuffer) { Camera *cam = getCamera(handle); if (!cam) return JNI_FALSE; FrameData frame = cam->CaptureFrame(); if (!frame.rgbData) return JNI_FALSE; unsigned char *dst = (unsigned char *)env->GetDirectBufferAddress(byteBuffer); if (!dst) { ReleaseFrame(frame); return JNI_FALSE; } size_t expected = (size_t)(frame.width * frame.height * 3); memcpy(dst, frame.rgbData, expected < frame.size ? expected : frame.size); ReleaseFrame(frame); return JNI_TRUE; } JNIEXPORT jint JNICALL Java_com_example_litecam_LiteCam_getFrameWidth(JNIEnv *, jobject, jint handle) { Camera *cam = getCamera(handle); if (!cam) return 0; return (jint)cam->frameWidth; } JNIEXPORT jint JNICALL Java_com_example_litecam_LiteCam_getFrameHeight(JNIEnv *, jobject, jint handle) { Camera *cam = getCamera(handle); if (!cam) return 0; return (jint)cam->frameHeight; } } // extern C 
Enter fullscreen mode Exit fullscreen mode

Step 2: CMake for JNI Build

The following CMakeLists.txt file is used to build the JNI shared library:

 cmake_minimum_required(VERSION 3.15) # Project name and version project(CameraProject VERSION 1.0 LANGUAGES CXX) # Set C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # Build type if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() # Define include directories set(INCLUDE_DIR ${CMAKE_SOURCE_DIR}/include) # Platform detection if(WIN32) set(PLATFORM_NAME "windows") elseif(APPLE) set(PLATFORM_NAME "macos") elseif(UNIX) set(PLATFORM_NAME "linux") else() set(PLATFORM_NAME "unknown") endif() # Architecture detection if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(ARCH_NAME "x86_64") else() set(ARCH_NAME "x86") endif() # Compiler-specific settings if(MSVC) # Set runtime library for Windows if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") else() set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") endif() # Enable parallel compilation add_compile_options(/MP) # Disable specific warnings add_compile_options(/wd4251 /wd4275) # Enable UTF-8 encoding add_compile_options(/utf-8) endif() # Define source files for the Camera library based on platform if (WIN32) set(LIBRARY_SOURCES src/CameraWindows.cpp src/CameraPreviewWindows.cpp ) elseif (UNIX AND NOT APPLE) set(LIBRARY_SOURCES src/CameraLinux.cpp src/CameraPreviewLinux.cpp ) elseif (APPLE) # Support universal binaries on macOS set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") # Ensure that Objective-C++ source files are compiled as Objective-C++ set(LIBRARY_SOURCES src/CameraMacOS.mm src/CameraPreviewMacOS.mm ) set_source_files_properties(src/CameraMacOS.mm src/CameraPreviewMacOS.mm PROPERTIES COMPILE_FLAGS "-x objective-c++") # Set main.cpp to be treated as Objective-C++ for macOS set_source_files_properties(src/main.cpp PROPERTIES COMPILE_FLAGS "-x objective-c++") endif() # Add JNI wrapper source (common for all platforms) list(APPEND LIBRARY_SOURCES src/LiteCamJNI.cpp ) # Define source files for the executable set(EXECUTABLE_SOURCES src/main.cpp ) # Add the Camera shared library add_library(litecam SHARED ${LIBRARY_SOURCES}) # Set library properties set_target_properties(litecam PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME "litecam" ) # Platform-specific library naming if(WIN32) set_target_properties(litecam PROPERTIES PREFIX "" SUFFIX ".dll" ) elseif(APPLE) set_target_properties(litecam PROPERTIES PREFIX "lib" SUFFIX ".dylib" ) else() set_target_properties(litecam PROPERTIES PREFIX "lib" SUFFIX ".so" ) endif() # Set include directories for the Camera library target_include_directories(litecam PUBLIC $<BUILD_INTERFACE:${INCLUDE_DIR}> $<INSTALL_INTERFACE:include> ) # Define the CAMERA_EXPORTS macro for the shared library target_compile_definitions(litecam PRIVATE CAMERA_EXPORTS LITECAM_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} LITECAM_VERSION_MINOR=${PROJECT_VERSION_MINOR} LITECAM_VERSION_PATCH=${PROJECT_VERSION_PATCH} ) # Platform-specific dependencies for the Camera library if (UNIX AND NOT APPLE) # Linux dependencies find_package(X11 REQUIRED) find_package(PkgConfig REQUIRED) # Check for Video4Linux2 pkg_check_modules(V4L2 libv4l2) if (X11_FOUND) target_include_directories(litecam PUBLIC ${X11_INCLUDE_DIR}) target_link_libraries(litecam PRIVATE ${X11_LIBRARIES} pthread) endif() if (V4L2_FOUND) target_include_directories(litecam PRIVATE ${V4L2_INCLUDE_DIRS}) target_link_libraries(litecam PRIVATE ${V4L2_LIBRARIES}) else() message(WARNING "Video4Linux2 not found - camera functionality may be limited") endif() elseif (APPLE) # macOS dependencies find_library(COCOA_LIBRARY Cocoa REQUIRED) find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED) find_library(COREMEDIA_LIBRARY CoreMedia REQUIRED) find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED) find_library(OBJC_LIBRARY objc REQUIRED) target_link_libraries(litecam PRIVATE ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${COREMEDIA_LIBRARY} ${COREVIDEO_LIBRARY} ${OBJC_LIBRARY} ) elseif (WIN32) # Windows dependencies target_link_libraries(litecam PRIVATE ole32 uuid mfplat mf mfreadwrite mfuuid ) endif() # JNI support - enhanced detection find_package(JNI) if (JNI_FOUND) target_include_directories(litecam PRIVATE ${JNI_INCLUDE_DIRS}) target_compile_definitions(litecam PRIVATE LITECAM_JNI_ENABLED) # Add JNI libraries on some platforms if(WIN32) # Windows doesn't typically need to link JNI libraries elseif(APPLE) # macOS typically has JNI in the framework else() # Linux might need explicit JNI library linking if(JNI_LIBRARIES) target_link_libraries(litecam PRIVATE ${JNI_LIBRARIES}) endif() endif() endif() # Optional: Add position independent code for shared library set_property(TARGET litecam PROPERTY POSITION_INDEPENDENT_CODE ON) # Add the camera_capture executable add_executable(camera_capture ${EXECUTABLE_SOURCES}) # Set executable properties set_target_properties(camera_capture PROPERTIES OUTPUT_NAME "camera_capture" ) # Link the Camera library to the executable target_link_libraries(camera_capture PRIVATE litecam) # Include the shared library's headers in the executable target_include_directories(camera_capture PRIVATE ${INCLUDE_DIR}) # For macOS, link against the frameworks for the executable too if (APPLE) target_link_libraries(camera_capture PRIVATE ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${COREMEDIA_LIBRARY} ${COREVIDEO_LIBRARY} ${OBJC_LIBRARY} ) endif() # Installation rules (optional) install(TARGETS litecam camera_capture EXPORT CameraProjectTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin INCLUDES DESTINATION include ) install(DIRECTORY ${INCLUDE_DIR}/ DESTINATION include FILES_MATCHING PATTERN "*.h" ) # Export targets for find_package support install(EXPORT CameraProjectTargets FILE CameraProjectTargets.cmake NAMESPACE CameraProject:: DESTINATION lib/cmake/CameraProject ) # Generate and install package config files include(CMakePackageConfigHelpers) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CameraProjectConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/CameraProjectConfig.cmake" INSTALL_DESTINATION lib/cmake/CameraProject ) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/CameraProjectConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ) 
Enter fullscreen mode Exit fullscreen mode

Step 3: The Java Camera Class with JNI

package com.example.litecam; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; public class LiteCam implements AutoCloseable { static { boolean loaded = false; try { loaded = loadBundled(); } catch (Throwable t) { } if (!loaded) { System.loadLibrary("litecam"); } } private static boolean loadBundled() throws Exception { String os = System.getProperty("os.name").toLowerCase(); String arch = System.getProperty("os.arch").toLowerCase(); String osToken; if (os.contains("win")) osToken = "windows"; else if (os.contains("mac") || os.contains("darwin")) osToken = "macos"; else if (os.contains("nux") || os.contains("linux")) osToken = "linux"; else return false; String archToken; if (arch.contains("aarch64") || arch.contains("arm64")) archToken = "arm64"; else if (arch.contains("64")) archToken = "x86_64"; else archToken = arch; // fallback String libBase = "litecam"; String ext = osToken.equals("windows") ? ".dll" : (osToken.equals("macos") ? ".dylib" : ".so"); String resourcePath = "/natives/" + osToken + "-" + archToken + "/" + (osToken.equals("windows") ? libBase + ext : "lib" + libBase + ext); try (java.io.InputStream in = LiteCam.class.getResourceAsStream(resourcePath)) { if (in == null) return false; java.nio.file.Path tempFile = java.nio.file.Files.createTempFile(libBase + "-", ext); try (java.io.OutputStream out = java.nio.file.Files.newOutputStream(tempFile)) { byte[] buf = new byte[8192]; int r; while ((r = in.read(buf)) != -1) out.write(buf, 0, r); } tempFile.toFile().deleteOnExit(); System.load(tempFile.toAbsolutePath().toString()); return true; } } private int handle = 0; // Native methods public static native String[] listDevices(); private native int open(int deviceIndex); private native void nativeClose(int handle); public native int[] listSupportedResolutions(int handle); public native boolean setResolution(int handle, int width, int height); public native boolean captureFrame(int handle, ByteBuffer rgbOut); public native int getFrameWidth(int handle); public native int getFrameHeight(int handle); public void openDevice(int index) { if (handle != 0) throw new IllegalStateException("Already opened"); handle = open(index); if (handle == 0) throw new RuntimeException("Failed to open camera index " + index); } public void closeDevice() { if (handle != 0) { nativeClose(handle); handle = 0; } } @Override public void close() { closeDevice(); } public List<int[]> getSupportedResolutions() { int[] flat = listSupportedResolutions(handle); List<int[]> list = new ArrayList<>(); if (flat != null) { for (int i=0;i+1<flat.length;i+=2) { list.add(new int[]{flat[i], flat[i+1]}); } } return list; } public boolean setResolution(int w, int h) { return setResolution(handle, w, h); } public int getWidth() { return getFrameWidth(handle); } public int getHeight() { return getFrameHeight(handle); } public boolean grabFrame(ByteBuffer dst) { return captureFrame(handle, dst); } public boolean isOpen() { return handle != 0; } } 
Enter fullscreen mode Exit fullscreen mode

Step 4: Build JNI Shared Library and JAR Package

  1. Build native library with CMake:

    mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release 
  2. Compile Java sources:

    cd .. javac -d build -h include java-src/com/example/litecam/*.java 
  3. Create JAR with native library:

    jar cf litecam.jar -C build com jar uf litecam.jar build/litecam.dll # or .dylib on macOS, .so on Linux 

Java Barcode Scanner Development

The following code snippet demonstrates the basic usage of LiteCam, ZXing, and Dynamsoft Barcode Reader APIs.

LiteCam

LiteCam cam = new LiteCam(); String[] devices = LiteCam.listDevices(); for (int i = 0; i < devices.length; i++) { System.out.println(i + ": " + devices[i]); } cam.openDevice(0); cam.setResolution(640, 480); ByteBuffer buffer = ByteBuffer.allocateDirect(640 * 480 * 3); if (cam.grabFrame(buffer)) { byte[] frameData = new byte[buffer.remaining()]; buffer.get(frameData); } cam.close(); 
Enter fullscreen mode Exit fullscreen mode

ZXing

import com.google.zxing.*; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.multi.GenericMultipleBarcodeReader; public class ZXingDetector { private MultiFormatReader reader; private GenericMultipleBarcodeReader multiReader; public void initialize() { reader = new MultiFormatReader(); multiReader = new GenericMultipleBarcodeReader(reader); } public List<Result> detectBarcodes(BufferedImage image) { List<Result> results = new ArrayList<>(); try { LuminanceSource source = new BufferedImageLuminanceSource(image); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); try { Result[] multiResults = multiReader.decodeMultiple(bitmap); results.addAll(Arrays.asList(multiResults)); } catch (NotFoundException e) { try { Result singleResult = reader.decode(bitmap); results.add(singleResult); } catch (NotFoundException ignored) { } } } catch (Exception e) { logger.debug("ZXing detection failed: {}", e.getMessage()); } return results; } } 
Enter fullscreen mode Exit fullscreen mode

Dynamsoft Barcode Reader

import com.dynamsoft.dbr.*; import com.dynamsoft.core.basic_structures.ImageData; public class DynamsoftDetector { private CaptureVisionRouter cvRouter; public void initialize() throws Exception { LicenseManager.initLicense("LICENSE-KEY"); cvRouter = new CaptureVisionRouter(); } public List<BarcodeResultItem> detectBarcodes(BufferedImage image) { List<BarcodeResultItem> results = new ArrayList<>(); try { ImageData imageData = createImageData(image); CapturedResult result = cvRouter.capture(imageData, EnumPresetTemplate.PT_READ_BARCODES); DecodedBarcodesResult barcodeResult = result.getDecodedBarcodesResult(); if (barcodeResult != null) { BarcodeResultItem[] items = barcodeResult.getItems(); if (items != null) { results.addAll(Arrays.asList(items)); } } } catch (Exception e) { logger.error("Dynamsoft detection failed: {}", e.getMessage()); } return results; } private ImageData createImageData(BufferedImage image) { } } 
Enter fullscreen mode Exit fullscreen mode

Source Code

https://github.com/yushulx/java-jni-barcode-qrcode-reader/tree/main/examples/barcode-scanner

Top comments (0)