Getting Started

If you would like to jump into some code directly, a good starting point for development with the Anyline SDK on Flutter, is to check out our Flutter repository on Github. In here, you can find example apps for iOS and Android with full source code for use cases that you can use as a starting point for your specialized scanning workflow.

Requirements

In order to use the Anyline SDK Flutter Plugin, please see to it that the following requirements are met:

Android

  • Android SDK Level >= 21

  • An Android device with decent camera functionality (recommended: 720p and adequate auto focus)

iOS

  • Minimum OS version: 12.0

For your iOS Xcode project, also make sure that the "Bitcode enabled" build setting is set to NO.

On your application’s Info.plist file, you are also required to include an entry for "Privacy - Camera Usage Description" with the value being the message you display to the user the first time the camera usage permission prompt is displayed.

Anyline 43.0.0 is a major rewrite of the scanner architecture and brings significant improvements to scanning performance and reliability. If you are coming from an older version, be aware of some breaking changes when upgrading to this version.

The most important change concerns JSON configuration. You will have to update your config files in JSON to properly initialize Anyline for Flutter. For more information, refer to the section on preparing a config file as well as upgrade guides for the respective iOS and Android platforms.

Anyline Flutter Guide

In this guide, we want to show you how you can make the most out of Anyline using our Flutter plugin. Feel free to contact us if you feel that it is incomplete or particular sections need elaboration.

Get an Anyline License

To be able to use the Flutter plugin you will have to get a license key by following the steps detailed in our documentation.

License <> Bundle Identifier

Every license is bound to a Bundle Identifier. If you change your Bundle Identifier, you will require a new license. This also ensures that your license key cannot be used in any other application.

Install the Anyline plugin

Add Anyline as a dependency in the pubspec.yaml:

dependencies:
  anyline_plugin: ^x.y.z

Install via pub.dev

Install the package dependencies from the command line:

$ flutter pub get

Alternatively, your IDE might support flutter pub get. Check their documentation to learn more.

Import Anyline into your Flutter project

Now in your Dart code, bring in Anyline with the following import:

import 'package:anyline_plugin/anyline_plugin.dart';

With this import, you can use the AnylinePlugin class to call methods accessing the SDK and scanning functionality. Proceed by creating an object of this class:

var anylinePlugin = AnylinePlugin();

Initialize the SDK

During app startup, before calling any other plugin method, initialize the Anyline SDK by calling the plugin object’s initSdk method, providing it with your license key:

try {
  await anylinePlugin.initSdk(licenseKey);
} catch (error) {
  print("error initializing Anyline SDK: $error");
}
Attempting to initialize the Anyline plugin before successfully calling initSdk will cause a LicenseException to be thrown.

Prepare a config file

Anyline uses JSON to configure how the plugin operates and behaves. You can find example configurations in our GitHub repository.

An example of a config JSON file:
{
    "cameraConfig": {
        "captureResolution": "1080p"
    },
    "flashConfig": {
        "mode": "manual",
        "alignment": "top_left"
    },
    "viewPluginConfig": {
        ...
    }
}

For more information and a general overview of how this works, please also visit our Documentation page.

Place your JSON config file(s) in a location which is declared to hold asset files in your application’s pubspec.yaml.

The Anyline plugin needs to be initialized with a JSON string, which you can accomplish by passing the path to the config file to rootBundle.loadString():

Future<String> loadJsonConfigFromFile() async {
  return await rootBundle.loadString("path/to/my/AnylineConfig.json");
}

Start scanning

With an instance of AnylinePlugin, call startScanning on it with the config string:

scanWithAnyline() async {
  var configJsonStr = loadJsonConfigFromFile();
  String? stringResult = await anylinePlugin.startScanning(configJsonStr);
}

Handling the results

If the scan was successful, the scan result would be returned by the startScanning function.

The result, which is represented as a JSON string, has a structure that depends on the type of plugin used. Shown below is an example of a result string returned from a meter scan:

{
  "fullImagePath" : "...",
  "pluginID" : "auto-meter",
  "confidence" : 99,
  "imagePath" : "...",
  "meterResult" : {
    "value" : "00613.839"
  },
  "cropRect" : {
    "y" : 410,
    "width" : 671,
    "height" : 298,
    "x" : 204
  }
}

In the case of the JSON result above, auto-meter is the name of the plugin’s unique ID from the config. The readout ("00613.839") could be found under meterResult > value. A confidence of 99 indicates Anyline’s level of confidence (0-100) in the result given.

The imagePath and fullImagePath properties hold the path to the scanned images saved within the device storage. Finally, cropRect provides the coordinates to the scanned object within the full sized image. Please read Displaying the Scanned Image on how to use the cached images in Flutter.

To obtain the readout, deserialize the string as a JSON object and then locate the relevant property.

The scanned value is always found within the xxxResult where xxx is the type of the plugin that was executed during the scan. In the example above, this value was in meterResult.

The following sections cover JSON result structure for other common plugin types. For a complete and up-to-date reference to the result object, please head to Plugin Result Parameters.

Meter

The Anyline Meter plugin is capable of scanning various types of electric, gas, and water meters. Furthermore, it is also possible to scan barcodes and QR codes, as well as serial numbers found on meters alongside their readings. Common 7-segment digital meters and "dial" meters could also be scanned by this plugin.

{
  "imagePath" : "...",
  "fullImagePath" : "...",
  "pluginID" : "meter",
  "confidence" : 100,
  "meterResult" : {
    "value" : "020793"
  },
  "cropRect" : {
    "y" : 410,
    "width" : 671,
    "height" : 298,
    "x" : 204
  }
}

Barcode

With the Anyline Barcode plugin, a large variety of barcode including QR code formats can be scanned. Furthermore, the plugin is often capable of scanning barcodes that are rotated at angles, which makes it easier to scan codes placed in certain ways.

{
  "fullImagePath" : "...",
  "imagePath" : "...",
  "barcodeResult" : {
    "barcodes" : [
      {
        "coordinates" : [ 865, 132, 868, 6, 254, -4, 251, 118 ],
        "value" : "1878266301",
        "base64Value" : "...",
        "format" : "CODE_128"
      }
    ]
  },
  "pluginID" : "barcode",
  "confidence" : 89,
  "cropRect" : {
    "y" : 739,
    "width" : 661,
    "height" : 440,
    "x" : 209
  }
}

The barcodes detected are returned as an array named barcode within barcodeResult. Each structure consists of the barcode rect (expressed as a series of four (X,Y) coordinate pairs), the Base64-encoded value of the barcode ("NDMwNjYyNw=="), and the barcode symbology (otherwise known as format).

License Plate

The License Plate plugin can be used to scan license plates of various sizes and from different countries.

The result of a license plate scan looks similar to this:

{
  "fullImagePath" : "...",
  "imagePath" : "...",
  "licensePlateResult" : {
    "country" : "D",
    "plateText" : "GLN 8988"
  },
  "pluginID" : "license-plate",
  "confidence" : 100,
  "cropRect" : {
    "y" : 425,
    "width" : 755,
    "height" : 188,
    "x" : 162
  }
}

ID

The Anyline ID plugin provides the functionality to scan passports, driving licenses, and other IDs including those that contain an MRZ (Machine Readable Zone).

The list of ID types supported by the ID Plugin is continuously being updated, and grows with each release. Please check the ID Plugin documentation for an up-to-date list for the current Anyline version.

{
  "imagePath": "...",
  "fullImagePath": "...",
  "pluginID": "id",
  "confidence": -1,
  "universalIdResult": {
    "result": {
      "dateOfBirth": {
        "textValues": {
          "latin": {
            "text": "10.10.1988",
            "confidence": 99
          }
        }
      },
      "documentNumber": {
        "textValues": {
          "latin": {
            "text": "PX20LJ641",
            "confidence": 99
          }
        }
      },
      "formattedDateOfBirth": {
        "textValues": {
          "latin": {
            "text": "10.10.1988",
            "confidence": 99
          }
        }
      },
      "firstName": {
        "textValues": {
          "latin": {
            "text": "JAN",
            "confidence": 99
          }
        }
      },
      ...
    },
    "visualization": {
      "contourPoints": [
        ...
      ],
      "contours": [
        ...
      ],
      "textRect": [ 392, 109, 377, 323 ]
    },
    "layoutDefinition": {
      "country": "DE",
      "layout": "DE_IDC_O_02001_F",
      "type": "idFront"
    }
  },
  "cropRect": {
    "y": 671,
    "width": 900,
    "height": 577,
    "x": 89
  }
}

The scanned details of each detected ID field is a node named after itself. Inside it is textValues, a map node holding values for the scanned text group according to script type. If there are multiple scripts found for the same field (for instance, Arabic or Cyrillic), they will be listed alongside the Latin value.

Under visualization, are coordinates to regions in the scanned image which you may use to locate the visible areas of the ID where Anyline was able to find matches.

layoutDefinition contains metadata about the scan results, including the type of document detected and its country of origin.

MRZ

{
  "fullImagePath": "...",
  "mrzResult": {
    "documentNumber": "PX20LJ641",
    "dateOfBirth": "881010",
    "sex": "M",
    "checkDigitDateOfExpiry": "5",
    "vizAddress": "66622 BIRLBACH\\nHAUPTSTRASSE 212",
    "allCheckDigitsValid": false,
    "mrzString": "IDD<<PX20LJ641<<<<<<<<<<<<<<<<\\n8810105<2606165D<<<<<<<<<<<<<8\\nNASS<<JAN<<<<<<<<<<<<<<<<<<<<<",
    "checkDigitDocumentNumber": "",
    "dateOfExpiry": "260616",
    "checkDigitDateOfBirth": "5",
    "givenNames": "JAN",
    "nationalityCountryCode": "D",
    "dateOfExpiryObject": "Sun Jun 16 00:00:00 UTC 2026",
    "checkDigitPersonalNumber": "",
    "checkDigitFinal": "8",
    "issuingCountryCode": "D",
    "fieldConfidences": {
      "dateOfBirth": 100,
      "nationalityCountryCode": 100,
      "givenNames": 99,
      ...
    },
    "dateOfBirthObject": "Sun Oct 10 00:00:00 UTC 1988",
    "surname": "NASS",
    "documentType": "ID"
  },
  "pluginID": "mrz",
  "confidence": 99,
  "imagePath": "...",
  "cropRect": {
    "y": 227,
    "width": 853,
    "height": 554,
    "x": 113
  }
}

Tire

The Anyline Tire plugin is capable of scanning Tire Identification Numbers (TIN), Tire Size Specifications, and Commercial Tire Identification Numbers.

The following example shows the result for a TIN scan.

{
 "imagePath" : "...",
  "fullImagePath" : "...",
  "tinResult" : {
    "text" : "DOTY9RJFPUU2618"
  },
  "pluginID" : "tire",
  "confidence" : 98,
  "cropRect" : {
    "y" : 897,
    "width" : 629,
    "height" : 125,
    "x" : 225
  }
}

Deserializing the JSON result

This section is based on JSON and Serialization from the Official Flutter Documentation.

As Dart does not come with built-in support for JSONs, you will have to deserialize them into a Map or your own object of choice.

Manually using dart:convert

For a quick start, using the library dart:convert provides you with JSON decoding and encoding utilities.

import 'dart:convert';

You can then jsonDecode the result string to convert it to a Map<String, dynamic>:

Map<String, dynamic> jsonResult = jsonDecode(stringResult);

Now you can access all the fields of the result JSON. For an overview of all the different result types that the Anyline Plugin returns, see above.

String imagePath = jsonResult['imagePath'];

Please be aware that by using this approach you lose all the benefits of type safety because Dart has no type information about the map fields (hence the dynamic keyword). This should only be used for trying our plugin out - for production use, model classes are highly recommended.

In our Flutter demo app this approach is used only because we do not process the data other than displaying it, and creating classes for all the different use cases would have gone beyond the scope of a demo app.

Creating model classes

For simple use cases like a voucher code scan result, creating a class yourself is recommended.

class VoucherResult {
String text;
String imagePath;
String fullImagePath;
num confidence;

    VoucherResult.fromJson(Map<String, dynamic> json)
      : text = json['text'] as String,
        imagePath = json['imagePath'] as String,
        fullImagePath = json['fullImagePath'] as String,
        confidence = json['confidence'] as int;
}

This class can be written to suit your needs. If you prefer to have the images saved not as string paths, but as File objects, just modify the fromJson() constructor to do so.

When dealing with more complex use cases, JSON serialization code generator libraries can make life easier for you.

Generating model classes from JSON

Flutter officially recommends two different libraries for JSON serialization code generation: json_serializable and built_value. They both allow you to define the model and never have to worry about maintaining any from/to JSON methods. This is extremely helpful if you want to make changes to your model later on - you do not have to change all of the occurences of a field in all methods, you just regenerate the methods. Visit the Flutter documentation to learn more about this.

As you probably only need one class for your Anyline result, the fastest way to generate your code without adding any further library dependencies is to use one of the online utilities out there, like JSON to Dart. Just paste the result JSON of your use case in there and it provides you with the whole generated Dart class.

In the following example the Driving License result was converted to a Dart class using JSON to Dart:

class DrivingLicenseResult {
  String surname;
  String givenNames;
  String dateOfBirth;
  String documentNumber;
  String dateOfExpiry;
  String dateOfIssue;
  String authority;
  String placeOfBirth;
  String dateOfExpiryObject;
  String dateOfIssueObject;
  String dateOfBirthObject;
  String drivingLicenseString;
  FieldConfidences fieldConfidences;
  String imagePath;
  String fullImagePath;
  int confidence;

  DrivingLicenseResult(
      {this.surname,
      this.givenNames,
      this.dateOfBirth,
      this.documentNumber,
      this.dateOfExpiry,
      this.dateOfIssue,
      this.authority,
      this.placeOfBirth,
      this.dateOfExpiryObject,
      this.dateOfIssueObject,
      this.dateOfBirthObject,
      this.drivingLicenseString,
      this.fieldConfidences,
      this.imagePath,
      this.fullImagePath,
      this.confidence});

  DrivingLicenseResult.fromJson(Map<String, dynamic> json) {
    surname = json['surname'];
    givenNames = json['givenNames'];
    dateOfBirth = json['dateOfBirth'];
    documentNumber = json['documentNumber'];
    dateOfExpiry = json['dateOfExpiry'];
    dateOfIssue = json['dateOfIssue'];
    authority = json['authority'];
    placeOfBirth = json['placeOfBirth'];
    dateOfExpiryObject = json['dateOfExpiryObject'];
    dateOfIssueObject = json['dateOfIssueObject'];
    dateOfBirthObject = json['dateOfBirthObject'];
    drivingLicenseString = json['drivingLicenseString'];
    fieldConfidences = json['fieldConfidences'] != null
        ? new FieldConfidences.fromJson(json['fieldConfidences'])
        : null;
    imagePath = json['imagePath'];
    fullImagePath = json['fullImagePath'];
    confidence = json['confidence'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['surname'] = this.surname;
    data['givenNames'] = this.givenNames;
    data['dateOfBirth'] = this.dateOfBirth;
    data['documentNumber'] = this.documentNumber;
    data['dateOfExpiry'] = this.dateOfExpiry;
    data['dateOfIssue'] = this.dateOfIssue;
    data['authority'] = this.authority;
    data['placeOfBirth'] = this.placeOfBirth;
    data['dateOfExpiryObject'] = this.dateOfExpiryObject;
    data['dateOfIssueObject'] = this.dateOfIssueObject;
    data['dateOfBirthObject'] = this.dateOfBirthObject;
    data['drivingLicenseString'] = this.drivingLicenseString;
    if (this.fieldConfidences != null) {
      data['fieldConfidences'] = this.fieldConfidences.toJson();
    }
    data['imagePath'] = this.imagePath;
    data['fullImagePath'] = this.fullImagePath;
    data['confidence'] = this.confidence;
    return data;
  }
}

class FieldConfidences {
  int authority;
  int categories;
  int dateOfBirth;
  int dateOfExpiry;
  int dateOfIssue;
  int documentNumber;
  int givenNames;
  int placeOfBirth;
  int surname;

  FieldConfidences(
      {this.authority,
      this.categories,
      this.dateOfBirth,
      this.dateOfExpiry,
      this.dateOfIssue,
      this.documentNumber,
      this.givenNames,
      this.placeOfBirth,
      this.surname});

  FieldConfidences.fromJson(Map<String, dynamic> json) {
    authority = json['authority'];
    categories = json['categories'];
    dateOfBirth = json['dateOfBirth'];
    dateOfExpiry = json['dateOfExpiry'];
    dateOfIssue = json['dateOfIssue'];
    documentNumber = json['documentNumber'];
    givenNames = json['givenNames'];
    placeOfBirth = json['placeOfBirth'];
    surname = json['surname'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['authority'] = this.authority;
    data['categories'] = this.categories;
    data['dateOfBirth'] = this.dateOfBirth;
    data['dateOfExpiry'] = this.dateOfExpiry;
    data['dateOfIssue'] = this.dateOfIssue;
    data['documentNumber'] = this.documentNumber;
    data['givenNames'] = this.givenNames;
    data['placeOfBirth'] = this.placeOfBirth;
    data['surname'] = this.surname;
    return data;
  }
}

JSON to Dart also supports nested JSONs, so a class for FieldConfidences was created as well.

With minimal effort you have your result class which you can still modify based on your needs. If you would like to store the date as a DateTime object rather than as a string, for instance, simply change the corresponding lines in the fromJson() and toJson() methods.

Why aren’t we providing generated result classes for all use cases?

We believe that you know best about your specific use case and therefore also know best about which fields you need and how you want them to be stored. The generation of Dart model classes only takes a few seconds using online tools while granting you full freedom on customizing the classes as you wish.

Quick Tips

Here you can find a collection of common use cases and how to deal with them. We hope these can spare you the extra trip to StackOverflow :)

Parsing Dates from an Anyline Result into a DateTime Object

If you followed the JSON deserialization techniques discussed in the previous section, date fields returned from, say, an ID scan are still presented as strings and not as DateTime objects. Unfortunately, the format DD.mm.yyyy cannot be parsed directly with DateTime.tryParse(). Instead, we have to make use of the Dart intl package and its DateFormat class:

import 'package:intl/intl.dart';

static DateTime parseAnylineDate(String date) {;
    return DateFormat('dd.MM.yyyy').parse(date);
}

The result fields named dateOf***Object use the date format DDD MMM HH:mm:ss Z YYYY which is not supported by the Dart intl library at the moment (because of missing timezone support). Instead, please use the corresponding dateOf*** fields.

Displaying the Scanned Image

You can access the image file from a scan result by passing its path into File(). You can then write it to the file system or just display it onscreen for verification:

var imageFile = File(imagePath)

This provides you with an Image widget which you can use in your app:

Image.file(imageFile)

Android ProGuard Configuration for Release Builds

When building your Android App in a Release Configuration, make sure to include the following lines in your proguard-rules.pro:

proguard-rules.pro
-keep public class * {
    public protected *;
}
-keep class at.nineyards.anyline.** { *; }
-dontwarn at.nineyards.anyline.**
-keep class org.opencv.** { *; }
-dontwarn org.opencv.**

Make sure to use the defined proguard-rules.pro in your app’s build.gradle, for instance as following:

app/build.gradle
release {
    minifyEnabled true
    shrinkResources true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    signingConfig signingConfigs.release
}