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 —
ReadPassportAndIdfor MRZ,ReadVINTextfor VIN. - Extending the open-source MRZ Scanner module requires adding the
vinmodelAAR dependency and creatingVINData,VINScanResult, and a sharedCommonResultbase class. - VIN scanning uses a narrow
DSRectscan 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?
This article is Part 1 in a 3-Part Series.
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
- Open the
mrz-scanner-mobile\android\samples\ScanMRZproject in Android Studio. -
Select File > New > Import Module, and add the
mrz-scanner-mobile\android\src\DynamsoftMRZScannerBundlemodule. Note: the module name isdynamsoftmrzscannerbundle.
-
Modify the dependency in the
build.gradlefile as follows:dependencies { implementation project(':dynamsoftmrzscannerbundle') } - 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
- Rename
MrzScannerConfig.javatoScannerConfig.javaand modify the class name accordingly. This class is responsible for configuring the scanner settings. -
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
- Rename
MrzScannerActivity.javatoScannerActivity.javaand modify the class name accordingly. This class is responsible for handling the camera preview and scanning process. -
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; } } -
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; } ... } -
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
-
Add a radio group in
activity_main.xmlto 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> -
In
MainActivity.java, pass the selected detection type toScannerActivity:@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); }); } -
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

MRZ Result

Android VIN Scanner
VIN Scanner UI

VIN Result

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 beforestartCapturing()and that the key covers both theDLR(label recognition, used for MRZ) andDBR(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 toDSRect(0.05f, 0.35f, 0.95f, 0.65f, true)if scans fail on a specific device. - VIN model AAR not bundled: If
ReadVINTextreturns no results, confirm thatcom.dynamsoft:vinmodel:1.0.0is listed in thedynamsoftmrzscannerbundlemodule’sdependenciesblock. 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