Build a Barcode Reader App With JavaFX and vlcj
The article shares how to create a cross-platform barcode reader with JavaFX and vlcj, which can capture video streams using vlcj and read barcodes from it.
Join the DZone community and get the full member experience.
Join For FreeThis 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.
JavaFX is an open-source, next-generation client application platform for desktop, mobile, and embedded systems built on Java.
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 We Will Do in This Article
- Setup a JavaFX developing environment.
- Write and run a barcode scanning application.
Environment Requirement
- 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.
- Install Eclipse. We choose Eclipse as the IDE.
- (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. 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.
You could specify the parameters of the project in the next step.
The structure of the new project is shown in the following image.
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.
Design the Interface and Link to A Controller Class
We can create layouts by code as well as using FXML. FXML can be used in pair 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:
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.
The final result is as below.
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
.
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.
x
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 {
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.
xxxxxxxxxx
private BarcodeReader br;
private Image currentImg;
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.
xxxxxxxxxx
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
.
xxxxxxxxxx
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.
xxxxxxxxxx
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;
}
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() {
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.
- 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.
- Download VlcjJavaFxApplication.java which is provided by the vlcj-javafx-demo and copy it to the project along with Main.java.
- 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
andgetImageView
). - The
play
method can accept two arguments: MRL and options. For example, you can use thedshow://
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:
xxxxxxxxxx
private VlcjJavaFxApplication vlcj;
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:
xxxxxxxxxx
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();
}
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.
xxxxxxxxxx
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.
xxxxxxxxxx
//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.
xxxxxxxxxx
public DecodingThread (MainController callback)
{
this.callback = callback;
}
public void run() {
//......
if (img==null) {
img=callback.getCurrentFrame();
}
//......
}
The callback method is also adapted for reading video stream.
xxxxxxxxxx
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:
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.
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.
To make the Open with SceneBuilder
context menu work, go to Windows
> Preferences
> JavaFX
, set up the path to Scene Builder.
Source Code
The source code of the project is available here: https://github.com/Dynamsoft/desktop-java-barcode-reader.
Published at DZone with permission of Lihang Xu. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments