How to Create a Cross-platform MRZ Scanner App Using Flutter and Dynamsoft Label Recognizer

Xiao Ling - Jun 28 '23 - - Dev Community

Machine-Readable Zone (MRZ) scanner applications are used for quick and automated data entry from identification documents such as passports, visas, ID cards, and other types of travel documents. Some industries such as banking, healthcare, hospitality, and transportation require MRZ scanning to verify the identity of their customers. With the increasing need for efficient and secure identity verification, MRZ scanning has become an essential feature for many applications. In this article, we will walk you through the steps to create a production-ready MRZ scanner app using Flutter and Dynamsoft Label Recognizer. The Flutter project can be compiled to run on Windows, Linux, Android, iOS and Web platforms.

Demo Video

Try Online Demo with Your Mobile Devices

MRZ scanner web demo

https://yushulx.me/flutter-MRZ-scanner/

Needed Flutter Plugins

  • flutter_ocr_sdk: Wraps Dynamsoft Label Recognizer with MRZ detection model.
  • image_picker: Supports picking images from the image library, and taking new pictures with the camera.
  • shared_preferences: Wraps platform-specific persistent storage for simple data.
  • camera: Provides camera preview for Android, iOS and web.
  • camera_windows: Provides camera preview for Windows.
  • share_plus: Shares content from your Flutter app via the platform's share dialog.
  • url_launcher: Launches a URL.

Procedure to Develop a MRZ Scanner Application Using Flutter

The initial step involves setting up a new Flutter project, installing the necessary dependencies and initializing the MRZ detection SDK with a valid license key. The license key can be obtained from the Dynamsoft Customer Portal:

  1. Create a new Flutter project.

    flutter create mrzscanner
    
  2. Add the following dependencies to the pubspec.yaml file.

    dependencies:
      flutter:
        sdk: flutter
    
      flutter_ocr_sdk: ^1.1.2 
      cupertino_icons: ^1.0.2
      image_picker: ^1.0.0
      shared_preferences: ^2.1.1
      camera: ^0.10.5+2
      camera_windows: 
        git:
          url: https://github.com/yushulx/flutter_camera_windows.git
      share_plus: ^7.0.2
      url_launcher: ^6.1.11
    
  3. Replace the contents in lib/main.dart with the following code:

    // main.dart
    import 'package:flutter/material.dart';
    import 'tab_page.dart';
    import 'dart:async';
    import 'global.dart';
    
    Future<void> main() async {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      Future<int> loadData() async {
        return await initMRZSDK();
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Dynamsoft MRZ Detection',
          theme: ThemeData(
            scaffoldBackgroundColor: colorMainTheme,
          ),
          home: FutureBuilder<int>(
            future: loadData(),
            builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
              if (!snapshot.hasData) {
                return const CircularProgressIndicator();
              }
              Future.microtask(() {
                Navigator.pushReplacement(context,
                    MaterialPageRoute(builder: (context) => const TabPage()));
              });
              return Container();
            },
          ),
        );
      }
    }
    
    // global.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_ocr_sdk/flutter_ocr_sdk.dart';
    import 'package:flutter_ocr_sdk/mrz_line.dart';
    
    FlutterOcrSdk mrzDetector = FlutterOcrSdk();
    
    Future<int> initMRZSDK() async {
      await mrzDetector.init(
          "LICENSE-KEY");
      return await mrzDetector.loadModel() ?? -1;
    }
    

In the subsequent sections, we will adhere to the UI design guidelines to fully develop the MRZ scanner application.

MRZ scanner app UI design

Home Page

The Home page contains a title, a description, a pair of buttons, a banner, and a bottom tab bar.

break down home page layout

  • Title:
  final title = Row(
      children: [
        Container(
            padding: const EdgeInsets.only(
              top: 30,
              left: 33,
            ),
            child: const Text('MRZ SCANNER',
                style: TextStyle(
                  fontSize: 36,
                  color: Colors.white,
                )))
      ],
    );
Enter fullscreen mode Exit fullscreen mode
  • Description:
  final description = Row(
      children: [
        Container(
            padding: const EdgeInsets.only(top: 6, left: 33, bottom: 44),
            child: const SizedBox(
              width: 271,
              child: Text(
                  'Recognizes MRZ code & extracts data from 1D-codes, passports, and visas.',
                  style: TextStyle(
                    fontSize: 18,
                    color: Colors.white,
                  )),
            ))
      ],
    );
Enter fullscreen mode Exit fullscreen mode
  • Buttons
  final buttons = Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        GestureDetector(
            onTap: () {
              if (!kIsWeb && Platform.isLinux) {
                showAlert(context, "Warning",
                    "${Platform.operatingSystem} is not supported");
                return;
              }

              Navigator.push(context, MaterialPageRoute(builder: (context) {
                return const CameraPage();
              }));
            },
            child: Container(
              width: 150,
              height: 125,
              decoration: BoxDecoration(
                color: colorOrange,
                borderRadius: BorderRadius.circular(10.0),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Image.asset(
                    "images/icon-camera.png",
                    width: 90,
                    height: 60,
                  ),
                  const Text(
                    "Camera Scan",
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(fontSize: 16, color: Colors.white),
                  )
                ],
              ),
            )),
        GestureDetector(
            onTap: () {
              scanImage();
            },
            child: Container(
              width: 150,
              height: 125,
              decoration: BoxDecoration(
                color: colorBackground,
                borderRadius: BorderRadius.circular(10.0),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Image.asset(
                    "images/icon-image.png",
                    width: 90,
                    height: 60,
                  ),
                  const Text(
                    "Image Scan",
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(fontSize: 16, color: Colors.white),
                  )
                ],
              ),
            ))
      ],
    );
Enter fullscreen mode Exit fullscreen mode
  • Banner:
  final image = Expanded(
        child: Image.asset(
      "images/image-mrz.png",
      width: MediaQuery.of(context).size.width,
      fit: BoxFit.cover,
    ));
Enter fullscreen mode Exit fullscreen mode

The tab bar is implemented with the TabBarView widget, which is created separately in the TabPage class. The TabPage class is defined as follows:

class TabPage extends StatefulWidget {
  const TabPage({super.key});

  @override
  State<TabPage> createState() => _TabPageState();
}

class _TabPageState extends State<TabPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final List<CustomTab> myTabs = <CustomTab>[
    CustomTab(
        text: 'Home',
        icon: 'images/icon-home-gray.png',
        selectedIcon: 'images/icon-home-orange.png'),
    CustomTab(
        text: 'History',
        icon: 'images/icon-history-gray.png',
        selectedIcon: 'images/icon-history-orange.png'),
    CustomTab(
        text: 'About',
        icon: 'images/icon-about-gray.png',
        selectedIcon: 'images/icon-about-orange.png'),
  ];

  int selectedIndex = 0;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: 3);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: TabBarView(
          controller: _tabController,
          children: const [
            HomePage(),
            HistoryPage(),
            AboutPage(),
          ],
        ),
        bottomNavigationBar: SizedBox(
          height: 83,
          child: TabBar(
            labelColor: Colors.blue,
            controller: _tabController,
            onTap: (index) {
              setState(() {
                selectedIndex = index;
              });
            },
            tabs: myTabs.map((CustomTab tab) {
              return MyTab(
                  tab: tab, isSelected: myTabs.indexOf(tab) == selectedIndex);
            }).toList(),
          ),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

As a tab is selected, the corresponding page is displayed. The MyTab class is a custom widget that displays the tab icon and text:

class MyTab extends StatelessWidget {
  final CustomTab tab;
  final bool isSelected;

  const MyTab({super.key, required this.tab, required this.isSelected});

  @override
  Widget build(BuildContext context) {
    return Tab(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Image.asset(
            isSelected ? tab.selectedIcon : tab.icon,
            width: 48,
            height: 32,
          ),
          Text(tab.text,
              overflow: TextOverflow.ellipsis,
              style: TextStyle(
                fontSize: 9,
                color: isSelected ? colorOrange : colorSelect,
              ))
        ],
      ),
    );
  }
}

class CustomTab {
  final String text;
  final String icon;
  final String selectedIcon;

  CustomTab(
      {required this.text, required this.icon, required this.selectedIcon});
}
Enter fullscreen mode Exit fullscreen mode

Camera Page

The Camera page features a full-screen camera preview along with an indicator that displays the scanning orientation. Given that OCR is orientation-sensitive, the role of the indicator is to guide users in adjusting the camera orientation, ensuring the MRZ code is scanned accurately.

MRZ scanning indicator

The UI of the camera page is built as follows:

class CameraPage extends StatefulWidget {
  const CameraPage({super.key});

  @override
  State<CameraPage> createState() => _CameraPageState();
}

class _CameraPageState extends State<CameraPage> with WidgetsBindingObserver {
  late CameraManager _mobileCamera;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);

    _mobileCamera = CameraManager(
        context: context,
        cbRefreshUi: refreshUI,
        cbIsMounted: isMounted,
        cbNavigation: navigation);
    _mobileCamera.initState();
  }
  ...

  List<Widget> createCameraPreview() {
    if (_mobileCamera.controller != null && _mobileCamera.previewSize != null) {
      return [
        SizedBox(
            width: MediaQuery.of(context).size.width <
                    MediaQuery.of(context).size.height
                ? _mobileCamera.previewSize!.height
                : _mobileCamera.previewSize!.width,
            height: MediaQuery.of(context).size.width <
                    MediaQuery.of(context).size.height
                ? _mobileCamera.previewSize!.width
                : _mobileCamera.previewSize!.height,
            child: _mobileCamera.getPreview()),
        Positioned(
          top: 0.0,
          right: 0.0,
          bottom: 0,
          left: 0.0,
          child: createOverlay(
            _mobileCamera.mrzLines,
          ),
        ),
      ];
    } 
  }

  @override
  Widget build(BuildContext context) {
    const hint = Text(
        'P<CANAMAN<<RITA<TANIA<<<<<<<<<<<<<<<<<<<<<<<\nERE82721<9CAN8412070M2405252<<<<<<<<<<<<<<08',
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        textAlign: TextAlign.start,
        style: TextStyle(
          fontSize: 12,
          color: Colors.white,
        ));

    return WillPopScope(
        onWillPop: () async {
          return true;
        },
        child: Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.black,
            title: const Text(
              'MRZ Scanner',
              style: TextStyle(color: Colors.white),
            ),
          ),
          body: Stack(
            children: <Widget>[
              if (_mobileCamera.controller != null &&
                  _mobileCamera.previewSize != null)
                Positioned(
                  top: 0,
                  right: 0,
                  left: 0,
                  bottom: 0,
                  child: FittedBox(
                    fit: BoxFit.cover,
                    child: Stack(
                      children: createCameraPreview(),
                    ),
                  ),
                ),
              const Positioned(
                left: 122,
                right: 122,
                bottom: 28,
                child: Text('Powered by Dynamsoft',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.white,
                    )),
              ),
              Positioned(
                bottom: (MediaQuery.of(context).size.height - 41 * 3) / 2,
                left: !kIsWeb && (Platform.isAndroid)
                    ? 0
                    : (MediaQuery.of(context).size.width - 41 * 9) / 2,
                child: !kIsWeb && (Platform.isAndroid)
                    ? Transform.rotate(
                        angle: pi / 2,
                        child: hint,
                      )
                    : hint,
              ),
            ],
          ),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

The CameraManager class controls camera access and returns camera frames for MRZ detection.

  • Start camera preview:
  Future<void> startVideo() async {
    mrzLines = null;

    isFinished = false;

    cbRefreshUi();

    if (kIsWeb) {
      webCamera();
    } else if (Platform.isAndroid || Platform.isIOS) {
      mobileCamera();
    } else if (Platform.isWindows) {
      _frameAvailableStreamSubscription?.cancel();
      _frameAvailableStreamSubscription =
          (CameraPlatform.instance as CameraWindows)
              .onFrameAvailable(controller!.cameraId)
              .listen(_onFrameAvailable);
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Stop camera preview:
  Future<void> stopVideo() async {
    if (controller == null) return;
    if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
      await controller!.stopImageStream();
    }

    controller!.dispose();
    controller = null;

    _frameAvailableStreamSubscription?.cancel();
    _frameAvailableStreamSubscription = null;
  }
Enter fullscreen mode Exit fullscreen mode
  • The methods of acquiring camera frames differ across Windows, Android, iOS, and Web platforms:
  // Web
  Future<void> webCamera() async {
    if (controller == null || isFinished || cbIsMounted() == false) return;

    XFile file = await controller!.takePicture();

    // process image

    if (!isFinished) {
      webCamera();
    }
  }

  // Windows
  void _onFrameAvailable(FrameAvailabledEvent event) {
    // process image
  }

  // Android & iOS
  Future<void> mobileCamera() async {
    await controller!.startImageStream((CameraImage availableImage) async {
      assert(defaultTargetPlatform == TargetPlatform.android ||
          defaultTargetPlatform == TargetPlatform.iOS);
      // process image
    });
  }
Enter fullscreen mode Exit fullscreen mode
  • Recognize MRZ by invoking the recognizeByBuffer method of the FlutterOcrSdk class:
  void processId(
      Uint8List bytes, int width, int height, int stride, int format) {
    cbRefreshUi();
    mrzDetector
        .recognizeByBuffer(bytes, width, height, stride, format)
        .then((results) {
      ...
    });
  }
Enter fullscreen mode Exit fullscreen mode

Result Page

The Result page contains the extracted MRZ data, a button to share the data with other applications and a button to save the data to the local storage.

MRZ scanner result page

The Share button is located in the AppBar widget.

AppBar(
  backgroundColor: Colors.black,
  title: const Text(
    'Result',
    style: TextStyle(color: Colors.white),
  ),
  actions: [
    Padding(
      padding: const EdgeInsets.only(right: 20),
      child: IconButton(
        onPressed: () {
          Map<String, dynamic> jsonObject =
              widget.information.toJson();
          String jsonString = jsonEncode(jsonObject);
          Share.share(jsonString);
        },
        icon: const Icon(Icons.share, color: Colors.white),
      ),
    )
  ],
)
Enter fullscreen mode Exit fullscreen mode

The extracted MRZ data is displayed in the Column widget.

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text("Document Type", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.type!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Issuing State", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.issuingCountry!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Surname", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.surname!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Given Name", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.givenName!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Passport Number", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.passportNumber!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Nationality", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.nationality!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Date of Birth (YYYY-MM-DD)", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.birthDate!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Gender", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.gender!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("Date of Expiry(YYYY-MM-DD)", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.expiration!, style: valueStyle),
    const SizedBox(
      height: 6,
    ),
    Text("MRZ String", style: keyStyle),
    const SizedBox(
      height: 3,
    ),
    Text(widget.information.lines!,
        style: const TextStyle(
            color: Colors.white,
            fontSize: 11,
            overflow: TextOverflow.ellipsis)),
    const SizedBox(
      height: 6,
    ),
  ],
)
Enter fullscreen mode Exit fullscreen mode

The Save button is located at the bottom of the page. As the button is tapped, the MRZ data is saved as a JSON string to the local storage.

MaterialButton(
  minWidth: 208,
  height: 45,
  onPressed: () async {
    Map<String, dynamic> jsonObject = widget.information.toJson();
    String jsonString = jsonEncode(jsonObject);
    final SharedPreferences prefs =
        await SharedPreferences.getInstance();
    var results = prefs.getStringList('mrz_data');
    if (results == null) {
      prefs.setStringList('mrz_data', <String>[jsonString]);
    } else {
      results.add(jsonString);
      prefs.setStringList('mrz_data', results);
    }

    close();
  },
  color: colorOrange,
  child: const Text(
    'Save and Continue',
    style: TextStyle(color: Colors.white, fontSize: 18),
  ),
)
Enter fullscreen mode Exit fullscreen mode

History Page

The History page presents a list of MRZ data that has been saved to the local storage. All data can be deleted by tapping the Delete button.

MRZ scanner history page

The ListView widget is used to display the list of MRZ data:

ListView.builder(
  itemCount: _mrzHistory.length,
  itemBuilder: (context, index) {
    return MyCustomWidget(
        result: _mrzHistory[index],
        cbDeleted: () async {
          _mrzHistory.removeAt(index);
          final SharedPreferences prefs =
              await SharedPreferences.getInstance();
          List<String> data =
              prefs.getStringList('mrz_data') as List<String>;
          data.removeAt(index);
          prefs.setStringList('mrz_data', data);
          setState(() {});
        },
        cbOpenResultPage: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => ResultPage(
                  information: _mrzHistory[index],
                  isViewOnly: true,
                ),
              ));
        });
  })
Enter fullscreen mode Exit fullscreen mode

The item of the list view is defined as MyCustomWidget, which contains two lines of MRZ data and a More button that opens a context menu. The context menu provides three options: delete, share and view.

class MyCustomWidget extends StatelessWidget {
  final MrzResult result;
  final Function cbDeleted;
  final Function cbOpenResultPage;

  const MyCustomWidget({
    super.key,
    required this.result,
    required this.cbDeleted,
    required this.cbOpenResultPage,
  });

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
        decoration: const BoxDecoration(color: Colors.black),
        child: Padding(
            padding: const EdgeInsets.only(top: 18, bottom: 16, left: 84),
            child: Row(
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      result.surname!,
                      style: const TextStyle(color: Colors.white),
                    ),
                    Text(
                      result.passportNumber!,
                      style: TextStyle(color: colorSubtitle),
                    ),
                  ],
                ),
                Expanded(child: Container()),
                Padding(
                  padding: const EdgeInsets.only(right: 27),
                  child: IconButton(
                    icon: const Icon(Icons.more_vert),
                    color: Colors.white,
                    onPressed: () async {
                      final RenderBox button =
                          context.findRenderObject() as RenderBox;

                      final RelativeRect position = RelativeRect.fromLTRB(
                        100,
                        button.localToGlobal(Offset.zero).dy,
                        40,
                        0,
                      );

                      final selected = await showMenu(
                        context: context,
                        position: position,
                        color: colorBackground,
                        items: [
                          const PopupMenuItem<int>(
                              value: 0,
                              child: Text(
                                'Delete',
                                style: TextStyle(color: Colors.white),
                              )),
                          const PopupMenuItem<int>(
                              value: 1,
                              child: Text(
                                'Share',
                                style: TextStyle(color: Colors.white),
                              )),
                          const PopupMenuItem<int>(
                              value: 2,
                              child: Text(
                                'View',
                                style: TextStyle(color: Colors.white),
                              )),
                        ],
                      );

                      if (selected != null) {
                        if (selected == 0) {
                          // delete
                          cbDeleted();
                        } else if (selected == 1) {
                          // share
                          Map<String, dynamic> jsonObject = result.toJson();
                          String jsonString = jsonEncode(jsonObject);
                          Share.share(jsonString);
                        } else {
                          // view
                          cbOpenResultPage();
                        }
                      }
                    },
                  ),
                ),
              ],
            )));
  }
}
Enter fullscreen mode Exit fullscreen mode

About Page

The About page showcases a title, a version number, a description, a button, and two links to the Dynamsoft website.

MRZ scanner about page

  • Title:
  final title = Container(
      padding: const EdgeInsets.only(top: 50, left: 39, bottom: 5, right: 39),
      child: Row(
        children: [
          Image.asset(
            "images/logo-dlr.png",
            width: MediaQuery.of(context).size.width - 80,
          ),
        ],
      ),
    );
Enter fullscreen mode Exit fullscreen mode
  • Version number:
  final version = Container(
      height: 40,
      padding: const EdgeInsets.only(left: 15, right: 15),
      child: Text(
        'App Version 2.2.20',
        style: TextStyle(color: colorText),
      ),
    );
Enter fullscreen mode Exit fullscreen mode
  • Description:
  final description = Container(
        padding: const EdgeInsets.only(left: 44, right: 39),
        child: const Center(
          child: Text(
            'Recognizes MRZ code & extracts data from 1D-codes, passports, and visas. Supports TD-1, TD-2, TD-3, MRV-A, and MRV-B standards.',
            style: TextStyle(color: Colors.white, wordSpacing: 2),
            textAlign: TextAlign.center,
          ),
        ));
Enter fullscreen mode Exit fullscreen mode
  • Button:
  final button = Container(
      padding: const EdgeInsets.only(top: 48, left: 91, right: 91, bottom: 69),
      child: MaterialButton(
        minWidth: 208,
        height: 44,
        color: colorOrange,
        onPressed: () {
          launchUrlString('https://www.dynamsoft.com/downloads/');
        },
        child: const Text(
          'GET FREE TRIAL SDK',
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
Enter fullscreen mode Exit fullscreen mode
  • Links:
  final links = Container(
      padding: const EdgeInsets.only(
        left: 15,
        right: 15,
      ),
      decoration: const BoxDecoration(color: Colors.black),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 13, bottom: 15),
            child: InkWell(
                onTap: () {
                  launchUrlString(
                      'https://www.dynamsoft.com/label-recognition/overview/');
                },
                child: Text(
                  'Dynamsoft Label Recognizer overview >',
                  style: TextStyle(color: colorOrange),
                )),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 13, bottom: 15),
            child: InkWell(
                onTap: () {
                  launchUrlString('https://www.dynamsoft.com/company/about/');
                },
                child: Text(
                  'Contact us >',
                  style: TextStyle(color: colorOrange),
                )),
          ),
        ],
      ),
    );
Enter fullscreen mode Exit fullscreen mode

Known Issues

When the application runs on desktop browsers, the camera frame is presented in a mirrored format. This has been found to interfere with the MRZ detection process. Therefore, the recommended workaround is to either anticipate the update of the official Flutter camera plugin or adjust the plugin's source code to resolve the issue.

Flutter camera mirrored preview

Source Code

https://github.com/yushulx/flutter-MRZ-scanner

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player