How to Create a Cross-platform MRZ Scanner App Using Flutter and Dynamsoft Label Recognizer
Tags: flutter, programming, mrz, ocr
Published: 2023-06-28 01:51:42

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](https://www.dynamsoft.com/label-recognition/overview/). The Flutter project can be compiled to run on **Windows**, **Linux**, **Android**, **iOS** and **Web** platforms.

## Demo Video
<video src="https://user-images.githubusercontent.com/2202306/248677414-bb10f353-bcfc-4b41-a1ef-fc7fcb6a25ef.mp4" controls="controls" muted="muted" style="max-height:640px; min-height: 200px; max-width: 100%;"></video>

## Try Online Demo with Your Mobile Devices

![MRZ scanner web demo](https://www.dynamsoft.com/codepool/img/2023/06/flutter-mrz-scanner-web-demo.png)

[https://yushulx.me/flutter-MRZ-scanner/](https://yushulx.me/flutter-MRZ-scanner/)

## Needed Flutter Plugins
- [flutter_ocr_sdk](https://pub.dev/packages/flutter_ocr_sdk): Wraps Dynamsoft Label Recognizer with MRZ detection model.
- [image_picker](https://pub.dev/packages/image_picker): Supports picking images from the image library, and taking new pictures with the camera.
- [shared_preferences](https://pub.dev/packages/shared_preferences): Wraps platform-specific persistent storage for simple data.
- [camera](https://pub.dev/packages/camera): Provides camera preview for Android, iOS and web.
- [camera_windows](https://github.com/yushulx/flutter_camera_windows.git):  Provides camera preview for Windows.
- [share_plus](https://pub.dev/packages/share_plus): Shares content from your Flutter app via the platform's share dialog.
- [url_launcher](https://pub.dev/packages/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](https://www.dynamsoft.com/customer/license/trialLicense?product=dlr):
1. Create a new Flutter project.
    ```bash
    flutter create mrzscanner
    ```

2. Add the following dependencies to the `pubspec.yaml` file.
    ```yaml
    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:
    ```dart
    // 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](https://xd.adobe.com/view/1b790db5-87ce-4b0f-aee9-6cf4dc7a4883-4e2a/grid) guidelines to fully develop the MRZ scanner application.

![MRZ scanner app UI design](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-ui-design.png)

### 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](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-home-page.png)

- Title:
  ```dart
  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,
                )))
      ],
    );
  ```
- Description:

  ```dart
  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,
                  )),
            ))
      ],
    );
  ```
- Buttons
  ```dart
  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),
                  )
                ],
              ),
            ))
      ],
    );
  ```

- Banner:

  ```dart
  final image = Expanded(
        child: Image.asset(
      "images/image-mrz.png",
      width: MediaQuery.of(context).size.width,
      fit: BoxFit.cover,
    ));
  ```

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

```dart
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(),
          ),
        ));
  }
}
```

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

```dart
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});
}
```


### 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](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-camera-indicator.jpg)

The UI of the camera page is built as follows:

```dart
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,
              ),
            ],
          ),
        ));
  }
}
```

The `CameraManager` class controls camera access and returns camera frames for MRZ detection.
- Start camera preview:
  
  ```dart
  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);
    }
  }
  ```

- Stop camera preview:

  ```dart
  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;
  }
  ```

- The methods of acquiring camera frames differ across Windows, Android, iOS, and Web platforms:
  
  ```dart
  // 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
    });
  }
  ```

- Recognize MRZ by invoking the `recognizeByBuffer` method of the `FlutterOcrSdk` class:

  ```dart
  void processId(
      Uint8List bytes, int width, int height, int stride, int format) {
    cbRefreshUi();
    mrzDetector
        .recognizeByBuffer(bytes, width, height, stride, format)
        .then((results) {
      ...
    });
  }
  ```

### 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](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-result-page.jpg)

The `Share` button is located in the `AppBar` widget.

```dart
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),
      ),
    )
  ],
)
```

The extracted MRZ data is displayed in the `Column` widget.

```dart
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,
    ),
  ],
)
```

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.

```dart
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),
  ),
)
```

### 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](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-history-page.jpg)

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

```dart
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,
                ),
              ));
        });
  })
```

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`.

```dart
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();
                        }
                      }
                    },
                  ),
                ),
              ],
            )));
  }
}
```

### 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](https://www.dynamsoft.com/codepool/img/2023/06/mrz-scanner-about-page.jpg)

- Title:
  ```dart
  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,
          ),
        ],
      ),
    );
  ```
- Version number:
  ```dart
  final version = Container(
      height: 40,
      padding: const EdgeInsets.only(left: 15, right: 15),
      child: Text(
        'App Version 2.2.20',
        style: TextStyle(color: colorText),
      ),
    );
  ```
- Description:
  ```dart
  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,
          ),
        ));
  ```
- Button:
  ```dart
  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),
        ),
      ),
    );
  ```
- Links:
  ```dart
  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),
                )),
          ),
        ],
      ),
    );
  ```

## 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](https://github.com/flutter/packages/tree/main/packages/camera/camera_web) to resolve the issue.

![Flutter camera mirrored preview](https://user-images.githubusercontent.com/2202306/248712168-7bdd7e57-7340-4149-a081-75f4622851aa.jpeg)

## Source Code
[https://github.com/yushulx/flutter-MRZ-scanner](https://github.com/yushulx/flutter-MRZ-scanner)