Effortlessly Recognize US Driver's Licenses in Android Apps

According to the American Association of Motor Vehicle Administrators (AAMVA) specification, US driver’s licenses store personal information using the PDF417 barcode symbology. Google’s Mobile Vision APIs can recognize PDF417 barcodes and extract driver’s license data, while the combination of Dynamsoft Barcode Reader and Dynamsoft Code Parser achieves the same goal. This article will guide you through integrating driver’s license recognition into Android apps using both solutions.

Recognizing Driver’s Licenses on Android

Prerequisites

Sample Driver’s License Image

Below is a sample image of a driver’s license for testing purposes:

driver's license

Driver’s License Recognition with Google Mobile Vision

Google’s Mobile Vision APIs can detect various mainstream barcode symbologies, including PDF417, which is commonly used on US driver’s licenses. The DriverLicense class provides predefined fields for extracting driver’s license information.

Quick Verification of Google’s API

To quickly test Google’s Mobile Vision API for driver’s license recognition, follow these steps:

  1. Clone the Android Vision sample code from GitHub.
  2. Create a new file named ResultActivity.java to display the driver’s license information:

     public class ResultActivity extends AppCompatActivity {
         @Override
         protected void onCreate(@Nullable Bundle savedInstanceState) {
        
             super.onCreate(savedInstanceState);
             TextView tv = new TextView(this);
             tv.setVerticalScrollBarEnabled(true);
             tv.setText("");
             tv.setMovementMethod(new ScrollingMovementMethod());
        
             Intent intent = getIntent();
             if (intent != null) {
                 Barcode.DriverLicense driverLicense = (Barcode.DriverLicense) intent.getParcelableExtra("DriverLicense");
                 if (driverLicense != null) {
                     String documentType = driverLicense.documentType;
                     tv.append("Document Type:\n" + documentType + "\n\n");
                     String firstName = driverLicense.firstName;
                     tv.append("First Name:\n" + firstName + "\n\n");
                     String middleName = driverLicense.middleName;
                     tv.append("Middle Name:\n" + middleName + "\n\n");
                     String lastName = driverLicense.lastName;
                     tv.append("Last Name:\n" + lastName + "\n\n");
                     String gender = driverLicense.gender;
                     tv.append("Gender: \n" + gender + "\n\n");
                     String addressStreet = driverLicense.addressStreet;
                     tv.append("Street:\n" + addressStreet + "\n\n");
                     String addressCity = driverLicense.addressCity;
                     tv.append("City:\n" + addressCity + "\n\n");
                     String addressState = driverLicense.addressState;
                     tv.append("State:\n" + addressState + "\n\n");
                     String addressZip = driverLicense.addressZip;
                     tv.append("Zip:\n" + addressZip + "\n\n");
                     String licenseNumber = driverLicense.licenseNumber;
                     tv.append("License Number:\n" + licenseNumber + "\n\n");
                     String issueDate = driverLicense.issueDate;
                     tv.append("Issue Date:\n" + issueDate + "\n\n");
                     String expiryDate = driverLicense.expiryDate;
                     tv.append("Expiry Date:\n" + expiryDate + "\n\n");
                     String birthDate = driverLicense.birthDate;
                     tv.append("Birth Date:\n" + birthDate + "\n\n");
                     String issuingCountry = driverLicense.issuingCountry;
                     tv.append("Issue Country:\n" + issuingCountry + "\n\n");
                 }
             }
        
             setContentView(tv);
         }
         @Override
         public void onBackPressed() {
             super.onBackPressed();
         }
     }
    
  3. In BarcodeCaptureActivity.java, locate the onBarcodeDetected(Barcode barcode) function and add the following code to start ResultActivity when a PDF417 barcode is detected:

     if (barcode.format == Barcode.PDF417) {
        
         Barcode.DriverLicense driverLicense = barcode.driverLicense;
         if (driverLicense != null) {
             Intent intent = new Intent(BarcodeCaptureActivity.this, ResultActivity.class);
             intent.putExtra("DriverLicense", driverLicense);
             startActivity(intent);
         }
     }
    
  4. Add the ResultActivity to your AndroidManifest.xml:

     <activity android:name=".ResultActivity" />
    
  5. Compile the project and run the app on your device.

    Google driver license

Reading Driver’s License with Dynamsoft Barcode Reader and Dynamsoft Code Parser

In this section, you’ll learn how to implement driver’s license recognition step by step using the Dynamsoft Barcode Reader and Dynamsoft Code Parser.

Android Project Configuration

  1. In your project’s build.gradle file, include the Dynamsoft Maven repository:

     allprojects {
         repositories {
             google()
             mavenCentral()
             maven { url "https://download2.dynamsoft.com/maven/aar" }
         }
     }
    
  2. In the module’s build.gradle file, add the following dependencies:

     implementation "com.dynamsoft:dynamsoftbarcodereaderbundle:10.2.1100"
     implementation "com.dynamsoft:dynamsoftcodeparser:2.2.11"
     implementation "com.dynamsoft:dynamsoftcodeparserdedicator:1.2.20"
    

    Explanation

    • The dynamsoftbarcodereaderbundle: Provides camera control and barcode decoding functionalities.
    • The dynamsoftcodeparser and dynamsoftcodeparserdedicator: Used for parsing PDF417 barcodes and extracting driver’s license information.

Activating Dynamsoft Barcode Reader

In the MainActivity.java file, initialize the license key as follows:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            LicenseManager.initLicense("LICENSE-KEY", this, (isSuccessful, error) -> {
                if (!isSuccessful) {
                    error.printStackTrace();
                    runOnUiThread(() -> ((TextView) findViewById(R.id.tv_license_error)).setText("License initialization failed: "+error.getMessage()));
                }
            });
        }
        ...
}

Creating a Fragment for Barcode Scanning

  1. Create a new layout file named fragment_scanner.xml in the layout folder:

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context=".fragments.ScannerFragment">
        
         <com.dynamsoft.dce.CameraView
             android:id="@+id/camera_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"/>
        
         <TextView
             android:id="@+id/tv_parsed"
             android:layout_margin="20dp"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textColor="@color/white"
             app:layout_constraintBottom_toBottomOf="parent" />
        
     </androidx.constraintlayout.widget.ConstraintLayout>
    

    The layout includes a CameraView for displaying the camera preview and a TextView showing parsed messages.

  2. Place a drivers-license.json template file in the assets folder. This template defines how to decode the PDF417 barcode and extract driver’s license information. Below is an example of the template:

     {
     	"BarcodeFormatSpecificationOptions" : 
     	[
     		{
     			"Name" : "bfs_pdf_417",
     			"BarcodeBytesLengthRangeArray" : 
     			[
     				{
     					"MaxValue" : 2147483647,
     					"MinValue" : 0
     				}
     			],
     			"BarcodeFormatIds" : 
     			[
     				"BF_DEFAULT",
     				"BF_POSTALCODE",
     				"BF_PHARMACODE",
     				"BF_NONSTANDARD_BARCODE",
     				"BF_DOTCODE"
     			],
     			"BarcodeHeightRangeArray" : null,
     			"BarcodeTextLengthRangeArray" : 
     			[
     				{
     					"MaxValue" : 2147483647,
     					"MinValue" : 0
     				}
     			],
     			"MinResultConfidence" : 30,
     			"MirrorMode" : "MM_NORMAL",
     			"PartitionModes" : 
     			[
     				"PM_WHOLE_BARCODE",
     				"PM_ALIGNMENT_PARTITION"
     			]
     		}
     	],
     	"BarcodeReaderTaskSettingOptions" : 
     	[
     		{
     			"Name" : "pdf_417_task",
     			"BarcodeColourModes" : 
     			[
     				{
     					"LightReflection" : 1,
     					"Mode" : "BICM_DARK_ON_LIGHT"
     				}
     			],
     			"BarcodeFormatSpecificationNameArray" : 
     			[
     				"bfs_pdf_417"
     			],
     			"DeblurModes" : null,
     			"LocalizationModes" : 
     			[
     				{
     					"Mode" : "LM_CONNECTED_BLOCKS"
     				},
     				{
     					"Mode" : "LM_LINES"
     				},
     				{
     					"Mode" : "LM_STATISTICS"
     				}
     			],
     			"MaxThreadsInOneTask" : 1,
     			"SectionImageParameterArray" : 
     			[
     				{
     					"ContinueWhenPartialResultsGenerated" : 1,
     					"ImageParameterName" : "ip_localize_barcode",
     					"Section" : "ST_REGION_PREDETECTION"
     				},
     				{
     					"ContinueWhenPartialResultsGenerated" : 1,
     					"ImageParameterName" : "ip_localize_barcode",
     					"Section" : "ST_BARCODE_LOCALIZATION"
     				},
     				{
     					"ContinueWhenPartialResultsGenerated" : 1,
     					"ImageParameterName" : "ip_decode_barcode",
     					"Section" : "ST_BARCODE_DECODING"
     				}
     			]
     		}
     	],
     	"CaptureVisionTemplates" : 
     	[
     		{
     			"Name" : "ReadPDF417",
     			"ImageROIProcessingNameArray" : 
     			[
     				"roi_pdf_417"
     			],
     			"ImageSource" : "",
     			"MaxParallelTasks" : 4,
     			"MinImageCaptureInterval" : 0,
     			"OutputOriginalImage" : 0,
     			"SemanticProcessingNameArray": [ "sp_pdf_417" ],
     			"Timeout" : 10000
     		}
     	],
     	"GlobalParameter" : 
     	{
     		"MaxTotalImageDimension" : 0
     	},
     	"ImageParameterOptions" : 
     	[
     		{
     			"Name" : "ip_localize_barcode",
     			"BinarizationModes" : 
     			[
     				{
     					"BinarizationThreshold" : -1,
     					"BlockSizeX" : 71,
     					"BlockSizeY" : 71,
     					"EnableFillBinaryVacancy" : 0,
     					"GrayscaleEnhancementModesIndex" : -1,
     					"Mode" : "BM_LOCAL_BLOCK",
     					"ThresholdCompensation" : 10
     				}
     			],
     			"GrayscaleEnhancementModes" : 
     			[
     				{
     					"Mode" : "GEM_GENERAL"
     				}
     			]
     		},
     		{
     			"Name" : "ip_decode_barcode",
     			"ScaleDownThreshold" : 99999
     		},
     		{
     			"Name": "ip_recognize_text",
     			"TextDetectionMode": {
     				"Mode": "TTDM_LINE",
     				"Direction": "HORIZONTAL",
     				"CharHeightRange": [
     					20,
     					1000,
     					1
     				],
     				"Sensitivity": 7
     			}
     		}
     	],
     	"TargetROIDefOptions" : 
     	[
     		{
     			"Name" : "roi_pdf_417",
     			"TaskSettingNameArray" : 
     			[
     				"pdf_417_task"
     			]
     		}
     	],
     	"CharacterModelOptions": [
     		{
                 "Name" : "NumberLetter"
             }
         ],
     	"SemanticProcessingOptions": [
     		{
     		  	"Name": "sp_pdf_417",
     		  	"ReferenceObjectFilter": {
     				"ReferenceTargetROIDefNameArray": [
     			  		"roi_pdf_417"
     				]
     			},
     			"TaskSettingNameArray": [
     				"dcp_pdf_417"
     			]
     		}
     	],
     	"CodeParserTaskSettingOptions": [
     		{
     			"Name": "dcp_pdf_417",
     			"CodeSpecifications": ["AAMVA_DL_ID","AAMVA_DL_ID_WITH_MAG_STRIPE","SOUTH_AFRICA_DL"]
     		}
     	]
     }
        
    
  3. Inflate the layout and set up the barcode scanning in the ScannerFragment.java file:

     package com.dynamsoft.dcv.driverslicensescanner.fragments;
    
     import android.app.AlertDialog;
     import android.os.Bundle;
     import android.view.LayoutInflater;
     import android.view.View;
     import android.view.ViewGroup;
        
     import androidx.annotation.NonNull;
     import androidx.fragment.app.Fragment;
     import androidx.lifecycle.ViewModelProvider;
     import androidx.navigation.fragment.NavHostFragment;
        
     import com.dynamsoft.core.basic_structures.CompletionListener;
     import com.dynamsoft.cvr.CaptureVisionRouter;
     import com.dynamsoft.cvr.CaptureVisionRouterException;
     import com.dynamsoft.cvr.CapturedResultReceiver;
     import com.dynamsoft.dbr.DecodedBarcodesResult;
     import com.dynamsoft.dce.CameraEnhancer;
     import com.dynamsoft.dce.CameraEnhancerException;
     import com.dynamsoft.dcp.ParsedResult;
     import com.dynamsoft.dcv.driverslicensescanner.FileUtil;
     import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
     import com.dynamsoft.dcv.driverslicensescanner.ParseUtil;
     import com.dynamsoft.dcv.driverslicensescanner.R;
     import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentScannerBinding;
        
     import java.util.Locale;
        
     public class ScannerFragment extends Fragment {
         private static final String TEMPLATE_ASSETS_FILE_NAME = "drivers-license.json";
         private static final String TEMPLATE_READ_PDF417 = "ReadPDF417";
         private FragmentScannerBinding binding;
         private CameraEnhancer mCamera;
         private CaptureVisionRouter mRouter;
         private MainViewModel viewModel;
        
         @Override
         public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
             binding = FragmentScannerBinding.inflate(inflater, container, false);
             viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
             viewModel.reset();
             mCamera = new CameraEnhancer(binding.cameraView, getViewLifecycleOwner());
             if (mRouter == null) {
                 initCaptureVisionRouter();
             }
             try {
                 mRouter.setInput(mCamera);
             } catch (CaptureVisionRouterException e) {
                 e.printStackTrace();
             }
             return binding.getRoot();
         }
        
         @Override
         public void onResume() {
             super.onResume();
             try {
                 mCamera.open();
             } catch (CameraEnhancerException e) {
                 e.printStackTrace();
             }
             mRouter.startCapturing(TEMPLATE_READ_PDF417, new CompletionListener() {
        
                 @Override
                 public void onSuccess() {
        
                 }
        
                 @Override
                 public void onFailure(int errorCode, String errorString) {
                     requireActivity().runOnUiThread(() ->
                             showDialog("Error", String.format(Locale.getDefault(), "ErrorCode: %d %nErrorMessage: %s", errorCode, errorString)));
                 }
             });
         }
        
         @Override
         public void onPause() {
             super.onPause();
             try {
                 mCamera.close();
             } catch (CameraEnhancerException e) {
                 e.printStackTrace();
             }
             mRouter.stopCapturing();
         }
        
         @Override
         public void onDestroyView() {
             super.onDestroyView();
             binding = null;
         }
        
         private void initCaptureVisionRouter() {
             mRouter = new CaptureVisionRouter(requireContext());
             try {
                 String template = FileUtil.readAssetFileToString(requireContext(), TEMPLATE_ASSETS_FILE_NAME);
                 mRouter.initSettings(template);
             } catch (CaptureVisionRouterException e) {
                 e.printStackTrace();
             }
             mRouter.addResultReceiver(new CapturedResultReceiver() {
                 @Override
                 public void onDecodedBarcodesReceived(DecodedBarcodesResult result) {
                     if (result.getItems().length > 0) {
                         viewModel.parsedText = result.getItems()[0].getText();
                     }
                 }
        
                 @Override
                 public void onParsedResultsReceived(ParsedResult result) {
                     if (result.getItems().length > 0) {
                         String[] displayStrings = ParseUtil.parsedItemToDisplayStrings(result.getItems()[0]);
                         if (displayStrings == null || displayStrings.length <= 1/*Only have Document Type content*/) {
                             showParsedText();
                             return;
                         }
                         viewModel.results = displayStrings;
                         requireActivity().runOnUiThread(() -> NavHostFragment.findNavController(ScannerFragment.this)
                                 .navigate(R.id.action_ScannerFragment_to_ResultFragment));
                         mRouter.stopCapturing();
                     } else {
                         showParsedText();
                     }
                 }
             });
         }
        
         private void showParsedText() {
             if (viewModel.parsedText != null && !viewModel.parsedText.isEmpty()) {
                 requireActivity().runOnUiThread(() -> {
                     if (binding != null) {
                         binding.tvParsed.setText("Failed to parse the result. The drivers' information does not exist in the barcode! ");
                     }
                 });
             }
         }
        
         private void showDialog(String title, String message) {
             new AlertDialog.Builder(requireContext())
                     .setCancelable(true)
                     .setPositiveButton("OK", null)
                     .setTitle(title)
                     .setMessage(message)
                     .show();
         }
     }
    

    Explanation

    • CameraEnhancer: Manages the camera preview.
    • CaptureVisionRouter: Handles the capture process, providing callbacks for decoded barcodes and parsed results.
    • initSettings: Loads the template file from the assets.
    • ParsedResult: Stores the parsed result.
    • viewModel: Shares data between fragments.

Creating a Fragment for Displaying Driver’s License Information

  1. Create a layout file named fragment_result.xml containing a ListView to display the driver’s license information:

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context=".fragments.ResultFragment">
        
         <ListView
             android:id="@+id/lv_result"
             android:padding="10dp"
             android:layout_width="match_parent"
             android:layout_height="match_parent"/>
        
     </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. Inflate the layout and load data from viewModel in the ResultFragment.java file:

     package com.dynamsoft.dcv.driverslicensescanner.fragments;
    
     import android.os.Bundle;
     import android.view.LayoutInflater;
     import android.view.View;
     import android.view.ViewGroup;
     import android.widget.ArrayAdapter;
        
     import androidx.annotation.NonNull;
     import androidx.fragment.app.Fragment;
     import androidx.lifecycle.ViewModelProvider;
        
     import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
     import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentResultBinding;
        
     public class ResultFragment extends Fragment {
         @Override
         public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
             FragmentResultBinding binding = FragmentResultBinding.inflate(inflater, container, false);
             MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
             ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, viewModel.results);
             binding.lvResult.setAdapter(adapter);
             return binding.getRoot();
         }
        
     }
    

    Dynamsoft driver's license

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/10.x/driver_license