Build an Android MRZ and VIN Scanner App with Dynamsoft Capture Vision

Dynamsoft’s pre-built Android MRZ (Machine Readable Zone) Scanner component, which wraps low-level Dynamsoft Capture Vision API, is open-source and available for download on GitHub. An Android VIN (Vehicle Identification Number) Scanner has not been officially released yet, but a relevant example project is available. This article will guide you through enhancing the existing Android MRZ Scanner component by adding VIN scanning capability.

What you’ll build: An Android app that scans both MRZ (passport and ID card) and VIN (Vehicle Identification Number) data by extending Dynamsoft’s open-source Android MRZ Scanner module with the Dynamsoft Capture Vision SDK.

Key Takeaways

  • The Dynamsoft Capture Vision SDK handles both MRZ and VIN scanning on Android by switching the template name — ReadPassportAndId for MRZ, ReadVINText for VIN.
  • Extending the open-source MRZ Scanner module requires adding the vinmodel AAR dependency and creating VINData, VINScanResult, and a shared CommonResult base class.
  • VIN scanning uses a narrow DSRect scan region instead of the full-frame MRZ guide overlay, keeping the UI context-appropriate for each mode.
  • This pattern applies to fleet management, vehicle rental, and border-control apps that need both document and vehicle ID scanning in a single component.

Common Developer Questions

  • How do I add VIN scanning to an existing Android MRZ scanner app?
  • What template names does Dynamsoft Capture Vision use for MRZ and VIN recognition on Android?
  • How do I configure a custom scan region in Android Camera Enhancer for VIN barcode reading?

Android MRZ and VIN Scanner in Action

Prerequisites

  • Get a 30-day free trial license for Dynamsoft Capture Vision.
  • Android MRZ Scanner: This official repository includes two projects: an Android Module and an Android Application. The module is the MRZ Scanner component, while the application demonstrates how to use it.
  • Android VIN Scanner: An official sample project demonstrating how to scan VINs using Dynamsoft Capture Vision.

Make sure to request a license key and download both projects before getting started.

Step 1: Import the MRZ Scanner Module into Your Project

  1. Open the mrz-scanner-mobile\android\samples\ScanMRZ project in Android Studio.
  2. Select File > New > Import Module, and add the mrz-scanner-mobile\android\src\DynamsoftMRZScannerBundle module. Note: the module name is dynamsoftmrzscannerbundle.

    Import Android Module

  3. Modify the dependency in the build.gradle file as follows:

     dependencies {
         implementation project(':dynamsoftmrzscannerbundle')
     }
    
  4. Build the project to ensure the module source code is ready for integration.

Step 2: Add VIN Recognition to the MRZ Scanner Module

MRZ and VIN recognition rely on model files that are encapsulated into AAR packages. To use them, navigate to the build.gradle file of the dynamsoftmrzscannerbundle module and add the following lines to the dependencies section:

dependencies {
    api "com.dynamsoft:capturevisionbundle:3.0.3000"
    api 'com.dynamsoft:vinmodel:1.0.0'
}

The Dynamsoft Capture Vision engine will automatically load the appropriate model files when you specify a template name in the startCapturing() method. For MRZ recognition, use the template name ReadPassportAndId, and for VIN recognition, use ReadVINText. No major code changes are required. We will only add support for the VIN scanning scenario and create corresponding data structures.

Create the Detection Type Enum

The source code was initially designed for MRZ scanning only. To support VIN scanning, create an EnumDetectionType.java file in the com.dynamsoft.mrzscannerbundle.ui package. This enum will distinguish between detection types:

package com.dynamsoft.mrzscannerbundle.ui;

public enum EnumDetectionType {
    MRZ,
    VIN,
}

Define the VIN Data Structure

Based on MRZData.java and MRZScanResult.java, create VINData.java and VINScanResult.java in the same package. These classes handle VIN data extraction and scan results.

  • VINData.java

      package com.dynamsoft.mrzscannerbundle.ui;
        
      import androidx.annotation.NonNull;
      import androidx.annotation.Nullable;
        
      import java.util.HashMap;
        
      public class VINData {
        
          public String vinString;
          public String wmi;
          public String region;
          public String vds;
          public String checkDigit;
          public String modelYear;
          public String plantCode;
          public String serialNumber;
        
          public static VINData extractItem(@Nullable HashMap<String, String> item) {
              if  (item == null) return null;
        
              VINData data = new VINData();
              data.vinString = item.get("vinString") == null ? "" : item.get("vinString");
              data.wmi = item.get("WMI") == null ? "" : item.get("WMI");
              data.region = item.get("region") == null ? "" : item.get("region");
              data.vds = item.get("VDS") == null ? "" : item.get("VDS");
              data.checkDigit = item.get("checkDigit") == null ? "" : item.get("checkDigit");
              data.modelYear = item.get("modelYear") == null ? "" : item.get("modelYear");
              data.plantCode = item.get("plantCode") == null ? "" : item.get("plantCode");
              data.serialNumber = item.get("serialNumber") == null ? "" : item.get("serialNumber");
        
              return data;
          }
        
          @NonNull
          public String toString() {
              return "VIN String: " + vinString + "\n" +
                      "WMI: " + wmi + "\n" +
                      "Region: " + region + "\n" +
                      "VDS: " + vds + "\n" +
                      "Check Digit: " + checkDigit + "\n" +
                      "Model Year: " + modelYear + "\n" +
                      "Manufacturer plant: " + plantCode + "\n" +
                      "Serial Number: " + serialNumber;
          }
      }
        
    
  • VINScanResult.java

      package com.dynamsoft.mrzscannerbundle.ui;
        
      import static com.dynamsoft.mrzscannerbundle.ui.ScannerActivity.EXTRA_RESULT;
      import static com.dynamsoft.mrzscannerbundle.ui.ScannerActivity.EXTRA_STATUS_CODE;
        
      import android.content.Intent;
        
      import java.util.HashMap;
        
      public class VINScanResult extends CommonResult {
          private VINData vinData;
        
          public VINScanResult(int resultCode, Intent data, EnumDetectionType detectionType) {
              if (data != null) {
                  super.resultStatus = data.getIntExtra(EXTRA_STATUS_CODE, resultCode);
                  assembleMap((HashMap<String, String>) data.getSerializableExtra(EXTRA_RESULT));
                  super.detectionType = detectionType;
              }
          }
        
          @Override
          public void assembleMap(HashMap<String, String> entry) {
              vinData = VINData.extractItem(entry);
          }
        
          public VINData getData() {
              return vinData;
          }
      }
        
    

Create a Shared Result Base Class

Create a shared CommonResult.java to handle common fields and methods for both MRZ and VIN scanning results. This class will be used as a base class for both MRZScanResult and VINScanResult.

package com.dynamsoft.mrzscannerbundle.ui;

import java.util.HashMap;

public class CommonResult {
    protected EnumDetectionType detectionType;
    @EnumResultStatus
    protected int resultStatus;
    protected String errorString;
    protected int errorCode;
    public void assembleMap(HashMap<String, String> entry) {}

    public EnumDetectionType getDetectionType() {
        return detectionType;
    }

    public @interface EnumResultStatus {
        int RS_FINISHED = 0;
        int RS_CANCELED = 1;
        int RS_EXCEPTION = 2;
    }

    @EnumResultStatus
    public int getResultStatus() {
        return resultStatus;
    }

    public String getErrorString() {
        return errorString;
    }

    public int getErrorCode() {
        return errorCode;
    }
}

Configure the Scanner for MRZ or VIN Mode

  1. Rename MrzScannerConfig.java to ScannerConfig.java and modify the class name accordingly. This class is responsible for configuring the scanner settings.
  2. Add support for detection type configuration:

     private EnumDetectionType detectionType = EnumDetectionType.MRZ;
    
     public EnumDetectionType getDetectionType() {
         return detectionType;
     }
    
     public void setDetectionType(EnumDetectionType detectionType) {
         this.detectionType = detectionType;
     }
    
    

Update Scanner Activity for Dual-Mode Support

  1. Rename MrzScannerActivity.java to ScannerActivity.java and modify the class name accordingly. This class is responsible for handling the camera preview and scanning process.
  2. Modify UI and logic based on the detection type:

     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_scanner);
    
         findViewById(R.id.btn_capture).setOnClickListener(v -> {
             enableCapture = true;
         });
    
         ...
    
         switch (configuration.getDetectionType()) {
             case MRZ:
             {
                 guideFrame.setVisibility(isGuideFrameVisible ? View.VISIBLE : View.GONE);
                 try {
                     mCamera.enableEnhancedFeatures(EnumEnhancerFeatures.EF_FRAME_FILTER);
                 } catch (CameraEnhancerException e) {
                     throw new RuntimeException(e);
                 }
             }
                 break;
             case VIN: {
                 guideFrame.setVisibility(View.GONE);
                 try {
                     mCamera.setScanRegion(new DSRect(0.1f, 0.4f, 0.9f, 0.6f, true));
                 } catch (CameraEnhancerException e) {
                     e.printStackTrace();
                 }
             }
                 break;
             default:
                 break;
         }
     }
    
  3. Configure the template for VIN scanning in the configCVR() method:

     private void configCVR() {
         ...
         switch (configuration.getDetectionType()) {
             case MRZ:
             {
                 if (configuration.getDocumentType() != null) {
                     switch (configuration.getDocumentType()) {
                         case DT_ALL:
                             mCurrentTemplate = "ReadPassportAndId";
                             break;
                         case DT_ID:
                             mCurrentTemplate = "ReadId";
                             break;
                         case DT_PASSPORT:
                             mCurrentTemplate = "ReadPassport";
                             break;
                     }
    
                 }
             }
                 break;
             case VIN:
                 mCurrentTemplate = "ReadVINText";
                 break;
         }
         ...
     }
    
  4. Parse the result accordingly:

     @Override
     public CommonResult parseResult(int i, @Nullable Intent intent) {
         switch (config.getDetectionType()) {
             case MRZ: {
                 return new MRZScanResult(i, intent, config.getDetectionType());
             }
    
             case VIN:
             {
                 return new VINScanResult(i, intent, config.getDetectionType());
             }
    
         }
    
         return null;
     }
    

Step 3: Update the App to Support MRZ and VIN Mode Switching

  1. Add a radio group in activity_main.xml to choose the scanning mode:

     <RadioGroup
         android:id="@+id/radio_group_mode"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         android:layout_centerHorizontal="true"
         android:layout_marginTop="40dp">
    
         <RadioButton
             android:id="@+id/radio_mrz"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="MRZ"
             android:checked="true"
             android:buttonTint="@color/orange"
             android:textColor="@android:color/black" />
    
         <RadioButton
             android:id="@+id/radio_vin"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="VIN"
             android:buttonTint="@color/orange"
             android:textColor="@android:color/black" />
     </RadioGroup>
    
  2. In MainActivity.java, pass the selected detection type to ScannerActivity:

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
    
         findViewById(R.id.btn_scan).setOnClickListener(v -> {
             Intent intent = new Intent(MainActivity.this, ScannerActivity.class);
             if (findViewById(R.id.radio_mrz).isChecked()) {
                 intent.putExtra("detectionType", EnumDetectionType.MRZ.name());
             } else {
                 intent.putExtra("detectionType", EnumDetectionType.VIN.name());
             }
             startActivity(intent);
         });
     }
    
  3. Display scan results based on the detection type:

     launcher = registerForActivityResult(new ScannerActivity.ResultContract(), commonResult -> {
             if (commonResult == null) return;
    
             if (commonResult.getResultStatus() == CommonResult.EnumResultStatus.RS_FINISHED) {
                 switch (commonResult.getDetectionType()) {
                     case MRZ: {
                         MRZScanResult result = (MRZScanResult) commonResult;
                         if (result.getData() != null) {
                             MRZData data = result.getData();
                             content.removeAllViews();
                             content.addView(childView("Name:", data.getFirstName() + " " + data.getLastName()));
                             content.addView(childView("Sex:", data.getSex() == null ? ""
                                     : data.getSex().substring(0, 1).toUpperCase() + data.getSex().substring(1)));
                             content.addView(childView("Age:", data.getAge() + ""));
                             content.addView(childView("Document Type:", data.getDocumentType()));
                             content.addView(childView("Document Number:", data.getDocumentNumber()));
                             content.addView(childView("Issuing State:", data.getIssuingState()));
                             content.addView(childView("Nationality:", data.getNationality()));
                             content.addView(childView("Date of Birth(YYYY-MM-DD):", data.getDateOfBirth()));
                             content.addView(childView("Date of Expiry(YYYY-MM-DD):", data.getDateOfExpire()));
                         }
                     }
                     break;
                     case VIN: {
                         VINScanResult result = (VINScanResult) commonResult;
                         if (result.getData() != null) {
                             VINData data = result.getData();
                             content.removeAllViews();
                             content.addView(childView("VIN:", data.vinString));
                             content.addView(childView("WMI:", data.wmi));
                             content.addView(childView("Region:", data.region));
                             content.addView(childView("VDS:", data.vds));
                             content.addView(childView("Check Digit:", data.checkDigit));
                             content.addView(childView("Model Year:", data.modelYear));
                             content.addView(childView("Plant Code:", data.plantCode));
                             content.addView(childView("Serial Number:", data.serialNumber));
                         }
                     }
                     break;
                 }
    
    
             } 
             ...
         });
    

Step 4: Run and Test the MRZ and VIN Scanner

Android MRZ Scanner

MRZ Scanner UI

Android MRZ Scanner module

MRZ Result

Android MRZ Scanner

Android VIN Scanner

VIN Scanner UI

Android VIN Scanner module

VIN Result

Android VIN Scanner

Common Issues & Edge Cases

  • License key not set or expired: The scanner will fail silently or show an error overlay if the Dynamsoft license key is missing or expired. Ensure DynamsoftLicenseManager.initLicense(...) is called before startCapturing() and that the key covers both the DLR (label recognition, used for MRZ) and DBR (barcode, used for VIN) modules.
  • VIN scan region too narrow on small screens: The DSRect(0.1f, 0.4f, 0.9f, 0.6f, true) region works well on standard phones but may clip long VIN barcodes on devices with unusual aspect ratios. Test on multiple screen sizes, or widen the region to DSRect(0.05f, 0.35f, 0.95f, 0.65f, true) if scans fail on a specific device.
  • VIN model AAR not bundled: If ReadVINText returns no results, confirm that com.dynamsoft:vinmodel:1.0.0 is listed in the dynamsoftmrzscannerbundle module’s dependencies block. The Capture Vision engine silently skips VIN recognition when the model file AAR is absent.

Source Code

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