Barcode Reader with JavaFX and vlcj

This article will guide you through the creation of a JavaFX GUI barcode reading application (as shown below) using Dynamsoft Barcode Reader SDK (DBR) and vlcj.

Main

JavaFX is an open-source, next-generation client application platform for desktop, mobile, and embedded systems built on Java.1

vlcj is a Java framework to allow an instance of a native VLC media player to be embedded in a Java application. With it, we can easily capture video streams in our JavaFX application.

To get familiar with using Dynamsoft Barcode Reader SDK in JavaFX, we can create a GUI application first.

What You Should Know

What We Will Do in This Article

  • Setup a JavaFX developing environment.
  • Write and run a barcode scanning application.

Environment Requirement

  1. Install the full version of Liberica JDK 11. Since Java 9, JavaFX is no longer packed with JDK. Liberica JDK provides a full version that includes JavaFX by default, which makes it convenient to develop and run JavaFX apps.
  2. Install Eclipse. We choose Eclipse as the IDE.
  3. (optional) Install e(fx)clipse plugin and Scene Builder. e(fx)clipse adds extra supports for JavaFX (e.g. FXML highlighting, Opening FXML with Scene Builder context menu) to eclipse. Scene Builder is a visual layout tool that lets users quickly design JavaFX application user interfaces without coding.2 If you don’t have them installed yet, please refer to the setup guide in the appendix.

Creating the GUI Barcode Reader

Create a JavaFX Maven Project

Go to File > New > Project > Maven, choose Maven project, use openjfx’s JavaFX FXML archetype.

choose_maven_archetype

You could specify the parameters of the project in the next step.

specify_parameters

The structure of the new project is shown in the following image.

project_structure

There are sample Java and FXML files in the project. Here, we just delete them.

Add Dependencies

Add dependencies of Dynamsoft Barcode Reader and vlcj in the pom.xml.

<dependencies>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-fxml</artifactId>
        <version>14</version>
    </dependency>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-swing</artifactId>
        <version>14</version>
    </dependency>
    <dependency>
        <groupId>uk.co.caprica</groupId>
        <artifactId>vlcj</artifactId>
        <version>4.5.0</version>
    </dependency>
    <dependency>
        <groupId>uk.co.caprica</groupId>
        <artifactId>vlcj-javafx</artifactId>
        <version>1.0.2</version>
    </dependency>
    <dependency>
        <groupId>com.dynamsoft</groupId>
        <artifactId>dbr</artifactId>
        <version>8.1.2</version>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>dbr</id>
        <url>https://download2.dynamsoft.com/maven/dbr/jar</url>
    </repository>
</repositories> 

vlcj’s javafx library uses the latest features of JavaFX like PixelBuffer to improve performance, so the minimum version of JavaFX is 13. JavaFX Swing is used to convert JavaFX’s Image to BufferedImage for DBR to decode.

We can create layouts by code as well as using FXML. FXML can be used in couple with Scene Builder. Here, we use FXML to create the layout.

First, create a new FXML named Main.fxml under this path: src/main/java/com/dynamsoft/BarcodeReader/fxml.

We use AnchorPane as the root element.

new_fxml

AnchorPane is a useful layout in JavaFX. It allows the edges of child nodes to be anchored to an offset from the anchor pane’s edges.3

The final result is as below.

scene_builder

FXML follows an MVC pattern. It needs a controller class to handle events. We can create a controller class and specify it in the FXML file. Here, we name it MainController.

controller

Specify the fx:id for nodes so that we can access them in the controller class.

For example, we need to access the TextField which is used to store the MRL of VLC.

FXML:

<TextField fx:id="mrlTextField" layoutX="164.0" layoutY="24.0" prefHeight="23.0" prefWidth="185.0" text="dshow://" />

Controller Class:

@FXML private TextField mrlTextField;

Specify the events for nodes so that we can handle them in the controller class.

FXML:

<Button fx:id="captureBtn" layoutX="30.0" layoutY="25.0" mnemonicParsing="false" onMouseClicked="#captureBtn_MouseClicked" text="Capture" />

Controller Class:

public void captureBtn_MouseClicked(Event event) throws Exception {

}

Create a class named Main as the main application and load the layout with it.

package com.dynamsoft.BarcodeReader;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Parent;
import javafx.scene.Scene;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/dynamsoft/BarcodeReader/fxml/Main.fxml"));
            Parent root = fxmlLoader.load();
            Scene scene = new Scene(root);
            primaryStage.setTitle("Barcode Reader");
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Read Barcodes from a Local Image

A canvas is used to show the image to decode and the detected areas of barcodes. Users can click the canvas to call FileChooser to select a local image file and then use DBR to read barcodes.

private BarcodeReader br;
private Image currentImg;
@Override
public void initialize(URL location, ResourceBundle resources) {
    try {
        br = new BarcodeReader("<your license key>");
    } catch (BarcodeReaderException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

public void cv_MouseClicked(Event event) {
    try {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Open Barcode File");
        File imgFile = fileChooser.showOpenDialog(Main.getPrimaryStage()); // FileChooser requires a stage. We can create a public static stage property in the Main class.
        currentImg =  new Image(imgFile.toURI().toString());        
        redrawImage(currentImg);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void readBtn_MouseClicked(Event event) throws Exception { 
    if (currentImg==null) {
        System.out.println("no img!");   
        return;
    }
    redrawImage(currentImg);
    updateRuntimeSettings();
    decodeImg(currentImg);
}

The major decoding operations lie in the decodeImg method. It will decode the current image, output the reading results and display the time elapsed.

private void decodeImg(Image img) throws BarcodeReaderException, IOException {
    Date startDate = new Date();
    Long startTime = startDate.getTime();
    Long endTime = null;
    
    TextResult[] results = br.decodeBufferedImage(SwingFXUtils.fromFXImage(img,null), "");
    
    Date endDate = new Date();
    endTime = endDate.getTime();

    StringBuilder sb = new StringBuilder(); 
    int index=0;
    for (TextResult result:results) {
        index=index+1;
        overlayCode(result);
        sb.append(index);
        sb.append("\n");
        sb.append("Type: ");
        sb.append(result.barcodeFormatString);
        sb.append("\n");
        sb.append("Text: ");
        sb.append(result.barcodeText);            
        sb.append("\n\n");            
    }
    resultTA.setText(sb.toString());
    StringBuilder timeSb = new StringBuilder();
    timeSb.append("Total: ");
    timeSb.append(endTime-startTime);
    timeSb.append("ms");
    timeLbl.setText(timeSb.toString());
}

There are some helper methods. redrawImage will reset the canvas with the new image. overlayCode will draw the outlines of detected barcodes on the canvas. unifyCoordinateReturnType is used to unify the coordinate unit to pixel. The unit of coordinate can be pixel or percentage. updateRuntimeSettings will try to init the runtime settings with the template and call unifyCoordinateReturnType.

private void redrawImage(Image img) {
    cv.setWidth(img.getWidth());
    cv.setHeight(img.getHeight());
    GraphicsContext gc = cv.getGraphicsContext2D();
    gc.drawImage(img, 0, 0, cv.getWidth(), cv.getHeight());
}

private void overlayCode(TextResult result) {
    GraphicsContext gc=cv.getGraphicsContext2D();

    List<Point> points= new ArrayList<Point>();
    for (Point point : result.localizationResult.resultPoints) {
        points.add(point);
    }
    points.add(result.localizationResult.resultPoints[0]);
    
    gc.setStroke(Color.RED);
    gc.setLineWidth(5);
    gc.beginPath();
    
    for (int i = 0;i<points.size()-1;i++) {
        Point point=points.get(i);
        Point nextPoint=points.get(i+1);
        gc.moveTo(point.x, point.y);
        gc.lineTo(nextPoint.x, nextPoint.y);            
    }
    gc.closePath();
    gc.stroke();
}

public void updateRuntimeSettings() throws BarcodeReaderException {
    String template = templateTA.getText();                    
    try {
        br.initRuntimeSettingsWithString(template,EnumConflictMode.CM_OVERWRITE);   
    }  catch (Exception e) {
        br.resetRuntimeSettings();
    }        
    unifyCoordinateReturnType();
}

private void unifyCoordinateReturnType() {
    PublicRuntimeSettings settings;
    try {
        settings = br.getRuntimeSettings();
        settings.resultCoordinateType=EnumResultCoordinateType.RCT_PIXEL;
        
        br.updateRuntimeSettings(settings);
    } catch (BarcodeReaderException e) {
        e.printStackTrace();
    }
}

Wrap the Decoding Process to a Thread

The decodeImg method above will block the UI thread. We can take a step further to wrap the decoding process to a Thread.

First, we pass the required data to the decoding thread through its constructor. Then, in order to pass the results from the decoding thread back to the main thread, we need a callback method. The part showing the decoding results has to be moved to the callback method.

Please note that if we want to update the UI of a JavaFX application, we need to call Platform.runLater or a Not On fx application thread error will occur.

class DecodingThread implements Runnable {
    private TextResult[] results;
    private Image img;
    private MainController callback;
    
    public DecodingThread (Image img, MainController callback)
    {
        this.img = img;
        this.callback = callback;
    }

    @Override
    public void run() {                        
        Date startDate = new Date();
        Long startTime = startDate.getTime();   
        
        try {
            results=br.decodeBufferedImage(SwingFXUtils.fromFXImage(img,null),"");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (BarcodeReaderException e) {
            e.printStackTrace();
        }
        Long endTime = null;
        Date endDate = new Date();
        endTime = endDate.getTime();
        Long timeElapsed = endTime-startTime;
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                callback.showResults(results, timeElapsed);
            }
        });            
    }
}

private void decodeImg(Image img) throws BarcodeReaderException, IOException, InterruptedException {     
    redrawImage(img);
    DecodingThread dt = new DecodingThread(img, this);
    Thread t = new Thread(dt);
    t.start();
}

//the callback method
public void showResults(TextResult[] results, Long timeElapsed) {
    StringBuilder sb = new StringBuilder(); 
    int index=0;
    for (TextResult result:results) {
        index=index+1;
        overlayCode(result);
        sb.append(index);
        sb.append("\n");
        sb.append("Type: ");
        sb.append(result.barcodeFormatString);
        sb.append("\n");
        sb.append("Text: ");
        sb.append(result.barcodeText);            
        sb.append("\n\n");            
    }
    resultTA.setText(sb.toString());
    StringBuilder timeSb = new StringBuilder();
    timeSb.append("Total: ");
    timeSb.append(timeElapsed);
    timeSb.append("ms");
    timeLbl.setText(timeSb.toString());
}

Read Barcodes from Video Stream

We want to read barcodes from various sources, such as local images, videos and Web/IP cameras. VLC is a good match and with vlcj, we can easily integrate it into our application.

  1. Install VLC. If you use a 32-bit JVM, then you should install the 32-bit version of VLC. If you use a 64-bit JVM, you should install the 64-bit version of VLC.
  2. Download VlcjJavaFxApplication.java which is provided by the vlcj-javafx-demo and copy it to the project along with Main.java.
  3. We want to show vlc player in a different window and use the Main window to control it. VlcjJavaFxApplication.java is simplified and added some media control methods (play and getImageView).
  4. The play method can accept two arguments: MRL and options. For example, you can use the dshow:// MRL to capture webcams and use options to control which camera to use. Two TextFields are used to store these settings.

Remember to make vlc a property to prevent it from being garbage collected, or the program will crash.

In MainController.java:

private VlcjJavaFxApplication vlcj;
@Override
public void initialize(URL location, ResourceBundle resources) {
    try {
    br = new BarcodeReader("<your license key>");
} catch (BarcodeReaderException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
    vlcj = new VlcjJavaFxApplication();
}

public void showVideoBtn_MouseClicked(Event event) throws Exception {
    if (vlcj.stage==null) {
        vlcj.start(new Stage());    
    } else {
        vlcj.stage.show();            
    }
    if (optionsTextField.getText()!="") {
        System.out.println("use options");
        System.out.println(optionsTextField.getText());            
        vlcj.play(mrlTextField.getText(),getOptions(optionsTextField.getText()));
    } else {
        vlcj.play(mrlTextField.getText());
    }
}

The VlcjJavaFxApplication.java:

public class VlcjJavaFxApplication extends Application {

    private MediaPlayerFactory mediaPlayerFactory;

    private EmbeddedMediaPlayer embeddedMediaPlayer;

    private ImageView videoImageView;
    public Stage stage;

    public VlcjJavaFxApplication() {
        mediaPlayerFactory = new MediaPlayerFactory();
        embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer();
    }

    @Override
    public final void start(Stage primaryStage) throws Exception {
        stage=primaryStage;
        videoImageView = new ImageView();
        videoImageView.setPreserveRatio(true);
        embeddedMediaPlayer.videoSurface().set(videoSurfaceForImageView(videoImageView));
        
        BorderPane root = new BorderPane();
        root.setStyle("-fx-background-color: black;");

        videoImageView.fitWidthProperty().bind(root.widthProperty());
        videoImageView.fitHeightProperty().bind(root.heightProperty());

        root.setCenter(videoImageView);

        Scene scene = new Scene(root, 1200, 675, Color.BLACK);
        primaryStage.setTitle("vlcj JavaFX");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public void play(String mrl) {
        embeddedMediaPlayer.media().play(mrl);
    }
    
    public void play(String mrl,String[] options) {
        embeddedMediaPlayer.media().play(mrl,options);
    }
    
    public ImageView getImageView() {
        return videoImageView;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

We can capture a frame and decode it manually.

private ScheduledExecutorService timer;
private Boolean found;
public Image getCurrentFrame() {
    return vlcj.getImageView().getImage();
}

//capture one frame from the video stream
public void captureBtn_MouseClicked(Event event) throws Exception {
    if (vlcj.stage!=null) {
        currentImg=getCurrentFrame();
        redrawImage(currentImg);
    }
}

It is also possible to do real-time barcode reading from the video stream with a timer.

//directly read from the video stream
public void readVideoStreamBtn_MouseClicked(Event event) throws Exception {
    found=false;
    updateRuntimeSettings();
    if (readVideoStreamBtn.getText()!="Stop") {
        readVideoStreamBtn.setText("Stop");
        decodeVideoStream();            
    } else {
        stopReadingVideoStream();
    }
}

private void decodeVideoStream() throws BarcodeReaderException, IOException, InterruptedException {
    DecodingThread dt = new DecodingThread(this);
    this.timer = Executors.newSingleThreadScheduledExecutor();
    this.timer.scheduleAtFixedRate(dt, 0, 100, TimeUnit.MILLISECONDS);
}

private void stopReadingVideoStream() {
    if (this.timer!=null) {
        this.timer.shutdownNow();
        this.timer=null;
        readVideoStreamBtn.setText("Read Video Stream");
    }
}

A property found is used to store the status of whether DBR has got results from the video stream so that decoding threads still running will not overwrite the detected results.

The decoding thread has a new constructor which only requires the callback argument. It will try to get the current frame as the image to decode.

public DecodingThread (MainController callback)
{
    this.callback = callback;
}
@Override
public void run() {
    //......
    if (img==null) {
        img=callback.getCurrentFrame();
    }
    //......
}

The callback method is also adapted for reading video stream.

public void showResults(Image img, TextResult[] results, Long timeElapsed) {
    if (found==true) {
        return;
    }
    redrawImage(img);
    if (results.length>0) {
        stopReadingVideoStream();    
        found=true;                    
        System.out.println("found");
    }
    //......
}

The final result:

video

Export to a Runnable Jar

All right! We have finished the application. We can right click on the project and export it to a runnable jar file to run it in other places.

runnable_jar

Appendix

Install e(fx)clipse and Scene Builder

To install e(fx)clipse, open Eclipse, go to Help > Install New Software, input the site URL (http://download.eclipse.org/efxclipse/updates-nightly/site/), select e(fx)clipse and click Finish.

Install e(fx)clipse

To make the Open with SceneBuilder context menu work, go to Windows > Preferences > JavaFX, set up the path to Scene Builder.

open_with_scenebuilder

scenebuilder_path

Source Code

The source code of the project is available here: https://github.com/Dynamsoft/desktop-java-barcode-reader

References

  1. https://openjfx.io/ 

  2. https://www.oracle.com/java/technologies/javase/javafxscenebuilder-info.html 

  3. https://docs.oracle.com/javase/8/javafx/api/javafx/scene/layout/AnchorPane.html