DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Creating a Flutter App to Scan MRZ, QR Codes, and Barcodes

flutter_ocr_sdk and flutter_barcode_sdk are two Flutter plugins built on top of the Dynamsoft Capture Vision SDK. They provide easy-to-use, cross-platform APIs for adding MRZ recognition and barcode scanning capabilities to Flutter apps. In this article, you'll learn how to create a Flutter app that integrates both plugins to scan machine-readable zones (MRZ) and 1D/2D barcodes. Instead of starting from scratch, we'll combine two existing example projects into a unified solution.

Demo Video: Flutter MRZ and Barcode Scanner

Prerequisites

Steps to Build an MRZ and Barcode Scanner App in Flutter

The app will include:

  • A toggle button to switch between MRZ and Barcode scanning modes
  • A button to load an image file for detection
  • A button to open the camera for live detection
  • A camera view for real-time MRZ and 1D/2D barcode scanning
  • A result view to display scan results from camera streams and images

We'll start by modifying the source code from the Flutter MRZ Scanner project to add UI and functionality.

Step 1: Install Dependencies

Add the following dependencies to your pubspec.yaml file:

flutter_barcode_sdk: ^3.0.4
flutter_ocr_sdk: ^2.0.4
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize the SDKs

Since both plugins wrap the same Dynamsoft SDK, you can initialize them with a shared license key in global.dart.

FlutterOcrSdk detector = FlutterOcrSdk();
FlutterBarcodeSdk barcodeReader = FlutterBarcodeSdk();
bool isLicenseValid = false;

Future<int> initSDK() async {
  String licenseKey =
      "LICENSE-KEY";
  int? ret = await detector.init(licenseKey);
  ret = await barcodeReader.init();
  ret = await barcodeReader.setLicense(licenseKey);

  if (ret == 0) isLicenseValid = true;
  return await detector.loadModel(modelType: ModelType.mrz) ?? -1;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Toggle MRZ and Barcode Modes

In home_page.dart, update the toggle button to switch between MRZ and Barcode modes:

ToggleButtons(
  borderRadius: BorderRadius.circular(10),
  isSelected: [isMrzSelected, !isMrzSelected],
  selectedColor: Colors.white,
  fillColor: Colors.orange,
  color: Colors.grey,
  children: const [
    Padding(
      padding: EdgeInsets.symmetric(horizontal: 20),
      child: Text('MRZ'),
    ),
    Padding(
      padding: EdgeInsets.symmetric(horizontal: 20),
      child: Text('Barcode'),
    ),
  ],
  onPressed: (index) {
    setState(() {
      isMrzSelected = (index == 0);
    });
  },
)
Enter fullscreen mode Exit fullscreen mode

The global boolean variable isMrzSelected determines which scanning mode is active.

Step 4: Scan from Image File

Update the scanImage() method in home_page.dart to perform MRZ or barcode recognition depending on the selected mode:

void openMrzResultPage(OcrLine information) {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => MrzResultPage(information: information),
    ),
  );
}

void openBarcodeResultPage(List<BarcodeResult> barcodeResults) {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => BarcodeResultPage(barcodeResults: barcodeResults),
    ),
  );
}


void scanImage() async {
  XFile? photo = await picker.pickImage(source: ImageSource.gallery);

  if (photo == null) {
    return;
  }

  if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
    File rotatedImage = await FlutterExifRotation.rotateImage(
      path: photo.path,
    );
    photo = XFile(rotatedImage.path);
  }

  Uint8List fileBytes = await photo.readAsBytes();

  ui.Image image = await decodeImageFromList(fileBytes);

  ByteData? byteData = await image.toByteData(
    format: ui.ImageByteFormat.rawRgba,
  );
  if (byteData != null) {
    if (isMrzSelected) {
      List<List<OcrLine>>? results = await detector.recognizeBuffer(
        byteData.buffer.asUint8List(),
        image.width,
        image.height,
        byteData.lengthInBytes ~/ image.height,
        ImagePixelFormat.IPF_ARGB_8888.index,
        ImageRotation.rotation0.value,
      );

      if (results != null && results[0].isNotEmpty) {
        openMrzResultPage(results[0][0]);
      } else {
        showAlert(context, "OCR Result", "Recognition Failed!");
      }
    } else {
      List<BarcodeResult>? results = await barcodeReader.decodeImageBuffer(
        byteData.buffer.asUint8List(),
        image.width,
        image.height,
        byteData.lengthInBytes ~/ image.height,
        ImagePixelFormat.IPF_ARGB_8888.index,
      );

      if (results.isNotEmpty) {
        openBarcodeResultPage(results);
      } else {
        showAlert(context, "Barcode Result", "Detection Failed!");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Both result pages already exist in the sample projects. Rename them to avoid conflicts.

Step 5: Real-time Scanning

In camera_manager.dart, update functions like webCamera(), processId(), and _decodeFrame() to call the appropriate APIs based on isMrzSelected. This enables real-time MRZ and barcode scanning across platforms.

Future<void> webCamera() async {
  _isWebFrameStarted = true;
  try {
    while (!(controller == null || isFinished || cbIsMounted() == false)) {
      XFile file = await controller!.takePicture();
      dynamic results;
      if (isMrzSelected) {
        results = await detector.recognizeFile(file.path);
        ocrLines = results;
      } else {
        results = await barcodeReader.decodeFile(file.path);
        barcodeResults = results;
      }

      if (results == null || !cbIsMounted()) return;

      cbRefreshUi();
      if (isReadyToGo && results != null) {
        handleResults(results);
      }
    }
  } catch (e) {
    print(e);
  }
  _isWebFrameStarted = false;
}

Future<void> processId(
      Uint8List bytes, int width, int height, int stride, int format) async {
  int rotation = 0;
  bool isAndroidPortrait = false;
  if (MediaQuery.of(context).size.width <
      MediaQuery.of(context).size.height) {
    if (Platform.isAndroid) {
      rotation = OCR.ImageRotation.rotation90.value;
      isAndroidPortrait = true;
    }
  }

  dynamic results;

  if (isMrzSelected) {
    ocrLines = await detector.recognizeBuffer(
        bytes, width, height, stride, format, rotation);
    results = ocrLines;
  } else {
    barcodeResults = await barcodeReader.decodeImageBuffer(
        bytes, width, height, stride, format);

    if (isAndroidPortrait &&
        barcodeResults != null &&
        barcodeResults!.isNotEmpty) {
      barcodeResults =
          rotate90barcode(barcodeResults!, previewSize!.height.toInt());
    }
    results = barcodeResults;
  }
  _isScanAvailable = true;
  if (results == null || !cbIsMounted()) return;

  cbRefreshUi();
  if (isReadyToGo && results != null) {
    handleResults(results!);
  }
}

Future<void> _decodeFrame(Uint8List rgb, int width, int height) async {
  if (isDecoding) return;

  isDecoding = true;
  dynamic results;
  if (isMrzSelected) {
    ocrLines = await detector.recognizeBuffer(
        rgb,
        width,
        height,
        width * 3,
        ImagePixelFormat.IPF_RGB_888.index,
        OCR.ImageRotation.rotation0.value);

    results = ocrLines;
  } else {
    barcodeResults = await barcodeReader.decodeImageBuffer(
        rgb, width, height, width * 3, ImagePixelFormat.IPF_RGB_888.index);
    results = barcodeResults;
  }

  if (cbIsMounted()) {
    cbRefreshUi();
    if (isReadyToGo && results != null) {
      handleResults(results!);
    }
  }

  isDecoding = false;
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Display Scan Overlays

To render overlays for MRZ and barcode results:

  1. Update createCameraPreview() in camera_page.dart:

    List<Widget> createCameraPreview() {
        Positioned widget;
        if (isMrzSelected) {
          widget = Positioned(
            top: 0.0,
            right: 0.0,
            bottom: 0,
            left: 0.0,
            child: createOverlay(_cameraManager.ocrLines),
          );
        } else {
          widget = Positioned(
            top: 0.0,
            right: 0.0,
            bottom: 0,
            left: 0.0,
            child: createOverlay(_cameraManager.barcodeResults),
          );
        }
    
        if (!kIsWeb &&
            (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
          return [
            SizedBox(width: 640, height: 480, child: _cameraManager.getPreview()),
            widget,
          ];
        } else {
          if (_cameraManager.controller != null &&
              _cameraManager.previewSize != null) {
            double width = _cameraManager.previewSize!.width;
            double height = _cameraManager.previewSize!.height;
            if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
              if (MediaQuery.of(context).size.width <
                  MediaQuery.of(context).size.height) {
                width = _cameraManager.previewSize!.height;
                height = _cameraManager.previewSize!.width;
              }
            }
    
            return [
              SizedBox(
                width: width,
                height: height,
                child: _cameraManager.getPreview(),
              ),
              widget,
            ];
          } else {
            return [const CircularProgressIndicator()];
          }
        }
      }
    
  2. Add overlay rendering logic for barcodes in global.dart:

    class OverlayPainter extends CustomPainter {
      List<List<OcrLine>>? ocrResults;
      List<BarcodeResult>? barcodeResults;
    
      OverlayPainter(dynamic results) {
        if (results is List<List<OcrLine>>) {
          ocrResults = results;
        } else if (results is List<BarcodeResult>) {
          barcodeResults = results;
        }
      }
    
      @override
      void paint(Canvas canvas, Size size) {
        final paint = Paint()
          ..color = Colors.blue
          ..strokeWidth = 5
          ..style = PaintingStyle.stroke;
    
        final textStyle = TextStyle(
          color: Colors.red, 
          fontSize: 16, 
        );
    
        if (ocrResults != null) {
          for (List<OcrLine> area in ocrResults!) {
            for (OcrLine line in area) {
              canvas.drawLine(Offset(line.x1.toDouble(), line.y1.toDouble()),
                  Offset(line.x2.toDouble(), line.y2.toDouble()), paint);
              canvas.drawLine(Offset(line.x2.toDouble(), line.y2.toDouble()),
                  Offset(line.x3.toDouble(), line.y3.toDouble()), paint);
              canvas.drawLine(Offset(line.x3.toDouble(), line.y3.toDouble()),
                  Offset(line.x4.toDouble(), line.y4.toDouble()), paint);
              canvas.drawLine(Offset(line.x4.toDouble(), line.y4.toDouble()),
                  Offset(line.x1.toDouble(), line.y1.toDouble()), paint);
    
              final textSpan = TextSpan(
                text: line.text,
                style: textStyle,
              );
    
              final textPainter = TextPainter(
                text: textSpan,
                textAlign: TextAlign.left,
                textDirection: TextDirection.ltr,
              );
    
              textPainter.layout();
    
              final offset = Offset(
                line.x1.toDouble(),
                line.y1.toDouble(),
              );
    
              textPainter.paint(canvas, offset);
            }
          }
        }
    
        if (barcodeResults != null) {
          for (var result in barcodeResults!) {
            double minX = result.x1.toDouble();
            double minY = result.y1.toDouble();
            if (result.x2 < minX) minX = result.x2.toDouble();
            if (result.x3 < minX) minX = result.x3.toDouble();
            if (result.x4 < minX) minX = result.x4.toDouble();
            if (result.y2 < minY) minY = result.y2.toDouble();
            if (result.y3 < minY) minY = result.y3.toDouble();
            if (result.y4 < minY) minY = result.y4.toDouble();
    
            canvas.drawLine(Offset(result.x1.toDouble(), result.y1.toDouble()),
                Offset(result.x2.toDouble(), result.y2.toDouble()), paint);
            canvas.drawLine(Offset(result.x2.toDouble(), result.y2.toDouble()),
                Offset(result.x3.toDouble(), result.y3.toDouble()), paint);
            canvas.drawLine(Offset(result.x3.toDouble(), result.y3.toDouble()),
                Offset(result.x4.toDouble(), result.y4.toDouble()), paint);
            canvas.drawLine(Offset(result.x4.toDouble(), result.y4.toDouble()),
                Offset(result.x1.toDouble(), result.y1.toDouble()), paint);
    
            TextPainter textPainter = TextPainter(
              text: TextSpan(
                text: result.text,
                style: const TextStyle(
                  color: Colors.yellow,
                  fontSize: 22.0,
                ),
              ),
              textAlign: TextAlign.center,
              textDirection: TextDirection.ltr,
            );
            textPainter.layout(minWidth: 0, maxWidth: size.width);
            textPainter.paint(canvas, Offset(minX, minY));
          }
        }
      }
    
      @override
      bool shouldRepaint(OverlayPainter oldDelegate) => true;
    }
    

Running the Flutter MRZ/Barcode Scanner App

Run the app on Windows, Linux, Android, iOS, and Web:

flutter run -d chrome    # Web
flutter run -d linux     # Linux
flutter run -d windows   # Windows
flutter run              # Android or iOS
Enter fullscreen mode Exit fullscreen mode

Flutter Windows MRZ and VIN scanner

Source Code

https://github.com/yushulx/flutter-barcode-mrz-document-scanner/tree/main/examples/barcode_mrz

Top comments (0)