Plugin Configuration

Anyline offers a wide range of scanning capabilities covering use cases for ID, License Plate, Barcode, Meter, and so on. A Plugin is a component of the Anyline SDK that implements a scanning use case.

It is possible to configure the scanning behavior either through a JSON configuration or via code. We’ll cover both in this section.

The Configuration object and JSON

A config object determines how a ScanPlugin, ScanViewPlugin, or ScanView appears or operates. Each of these three components can be initialized using the following config objects:

  • An ALScanPluginConfig configures an ALScanPlugin,

  • An ALScanViewPluginConfig configures an ALScanViewPlugin, and

  • An ALScanViewConfig configures an ALScanView.

To initialize a config object, pass a child (or children) config object corresponding to its type, or its JSON representation in an NSDictionary, as a parameter in its initializer.

For an in-depth guide on how to structure your configurations through JSON, please refer to Anyline View Configuration.

To give an example, take the ALScanPluginConfig class (NOTE: this is simplified):

@interface ALScanPluginConfig : NSObject

@property (nonatomic, readonly) ALPluginConfig *pluginConfig;

@end

It owns an ALPluginConfig which contains within it the configuration details for all possible use cases supported by Anyline. An example of such a use case, Meter Scanning, can be traced back to a property named meterConfig under ALPluginConfig, the interface of which looks like the following:

@interface ALMeterConfig : NSObject

@property (nonatomic, assign) ALMeterConfigScanMode *scanMode;
@property (nonatomic, nullable, strong) NSNumber *minConfidence;

@end

To construct an Anyline component or its config object, it is usually more convenient to prepare an NSDictionary holding the JSON configuration for your use case, and then create the Anyline component or config object using initializer methods that accept the dictionary as a parameter:

NSError *error;

// the JSON configuration string (here hardcoded, but more typically loaded from a file included with the app bundle)
NSString *pluginConfigStr = @"{\"pluginConfig\":...}";

// convert to a dictionary type
NSDictionary *pluginConfigDict = [pluginConfigStr asJSONObject];

// initialize the Scan Plugin with the config dictionary
ALScanPlugin *scanPlugin = [[ALScanPlugin alloc] initWithJSONDictionary:pluginConfigDict error:&error];

// check the error object and handle if necessary

Alternative methods to initialize these components are also available, for when you want additional run-time control over the configuration, or if you want to forego passing an error object. However, provided that you structure your config JSON correctly, the above example remains the simplest way to set up your Anyline scan components.

You may use the NSString extension methods

  • -[toJSONObject:error:]

  • -[asJSONObject:]

to transform a string containing your JSON configuration into a suitable NSDictionary type for your Anyline class initializers.

If deserializing the JSON string is not possible, then both functions will return nil. In addition, if using the first version, the error reference parameter will provide the reason why the operation failed to complete successfully.

Please also check the ALJSONExtras extension methods in ALJSONUtilities.h for other methods that may be useful to you.

Configs

ScanPluginConfig

A ScanPluginConfig determines, among other things, the scanning use case and essential characteristics of an Anyline plugin’s scanning behavior. Its main purpose is to be used to initialize an ALScanPlugin object, whose constructor can take this config object as a parameter.

Inside it, an ALScanPlugin holds an ALPluginConfig object which offer further customization possibilities for specific uses cases.

It is perhaps easier to see this config object through its JSON representation (which can also be obtained via the asJSONString method). Here we give one such representation of a scan plugin config designed to scan electric meters:

"pluginConfig": {
    "meterConfig": {
        "scanMode": "auto_analog_digital_meter",
        "id": "METER",
        "cancelOnResult": true
    },
}

From the JSON above, one can see that the ScanPluginConfig consists of a PluginConfig property named meterConfig, which further contains specific information that it is going to be run with the auto_analog_digital_meter mode.

For additional details regarding plugin configs, check ALPluginConfig.h.

To initialize a ScanPlugin, pass a dictionary based on the JSON config object above:

ALScanPlugin *scanPlugin = [[ALScanPlugin alloc] initWithJSONDictionary:configDict error:&error];

or with an ALScanPluginConfig object (which is essentially formed using the same configDict object):

ALScanPluginConfig *scanPluginConfig = [[ALScanPluginConfig alloc] initWithJSONDictionary:configDict error:&error];

// check error to see if you can safely proceed with the next operation

ALScanPlugin *scanPlugin = [[ALScanPlugin alloc] initWithConfig:scanPluginConfig];
As soon as a Plugin component is instantiated with its config object, you will no longer be able to change the object during runtime.

ScanViewPluginConfig

A ScanViewPluginConfig is an aggregate config structure that holds a ScanPluginConfig, a CutoutConfig, and a ScanFeedbackConfig:

  • The ScanPluginConfig, as discussed in the previous section, determines the use case and scanning behavior of the plugin.

  • A CutoutConfig includes details about the camera view cutout to be displayed on the scan view, including where it lies within the scan view, or how much space the cutout region occupies.

  • A ScanFeedbackConfig holds properties to help you configure how the objects of interest positioned within the camera cutout are visually treated, and what happens when a scan result is found.

An example JSON representation of a ScanViewPluginConfig is shown below for the meter use case:

"viewPluginConfig": {
    "pluginConfig": {
        "id": "METER",
        "meterConfig": {
            "scanMode": "auto_analog_digital_meter"
        },
        "cancelOnResult": true
    },
    "cutoutConfig": {
        "maxWidthPercent": "75%",
        "maxHeightPercent": "90%",
        "ratioFromSize": { "width": 7, "height": 3 },
        "cornerRadius": 8,
        "strokeColor": "#8be9fd",
        "strokeWidth": 3
    },
    "scanFeedbackConfig": {
        "style": "rect",
        "strokeWidth": 2,
        "strokeColor": "#0099FF",
        "fillColor": "#220099FF",
        "beepOnResult": true,
        "vibrateOnResult": true,
        "blinkAnimationOnResult": true
    }
}

To create a ScanPluginConfig, prepare an NSDictionary from a JSON object like the one above as you would with the ALScanPluginConfig, and then pass it as a parameter to its initializer:

ALScanViewPluginConfig *scanViewPluginConfig = [[ALScanViewPluginConfig alloc] initWithJSONDictionary:configDictionary
                                                                                                error:&error];
ALScanViewPlugin *scanViewPlugin = [[ALScanViewPlugin alloc] initWithConfig:scanViewPluginConfig error:&error];
When creating an ALScanViewPlugin in this way, a corresponding ScanPlugin is also automatically created (based on the details found within pluginConfig node of the JSON) and becomes part of the ScanViewPlugin.

Or alternatively, you can create an ALScanViewPluginConfig object using an ALScanPluginConfig, along with (optionally) an ALCutoutConfig and an ALScanFeedbackConfig:

ALScanViewPluginConfig *scanViewPluginConfig = [[ALScanViewPluginConfig alloc] initWithScanPluginConfig:scanPluginConfig
                                                                                           cutoutConfig:cutoutConfig
                                                                                     scanFeedbackConfig:scanFeedbackConfig
                                                                                                  error:&error];
// check the error object and handle appropriately

ALScanViewPlugin *scanViewPlugin = [[ALScanViewPlugin alloc] initWithConfig:scanViewPluginConfig
                                                                      error:&error];

// check the error object and handle appropriately

ScanViewConfig

An ALScanViewConfig contains configuration details controlling the camera and flash (torch) modules of the ScanView, both of which are also represented in Anyline by their own config classes (respectively, ALCameraConfig and ALFlashConfig).

An aggregated example of the configs in JSON form can look like the following:

"cameraConfig": {
    "captureResolution": "1080p"
},
"flashConfig": {
    "mode": "manual",
    "alignment": "bottom_left",
    "offset": { "x": 0, "y": -40 }
},

To create an ALScanViewConfig, form it in the usual way via JSON or through its constituent config types:

Initialize a ScanViewConfig using JSON
ALScanViewConfig *scanViewConfig = [[ALScanViewConfig alloc] initWithJSONDictionary:JSONDict error:&error];
ALScanView *scanView = [[ALScanView alloc] initWithFrame:frame
                                          scanViewPlugin:scanViewPlugin
                                          scanViewConfig:scanViewConfig
                                                   error:&error];
Initialize a ScanViewConfig using ALCameraConfig and ALFlashConfig objects
ALScanViewConfig *scanViewConfig = [[ALScanViewConfig alloc] initWithCameraConfig:cameraConfig
                                                                      flashConfig:flashConfig];
ALScanView *scanView = [[ALScanView alloc] initWithFrame:frame
                                          scanViewPlugin:scanViewPlugin
                                          scanViewConfig:scanViewConfig
                                                   error:&error];

CameraConfig and FlashConfig

The FlashConfig allows you to configure the torch module, which can include the visibility and location of the flash toggle button within the scan view.

The CameraConfig defines a few properties determining the camera module used. At the moment, the main property is zoomGesture, which, when true, allows pinch-to-zoom behavior for the running scan view.

For the time being, only 1080 is supported for the Camera Config property captureResolution.

It is possible to create ALCameraConfig and ALFlashConfig individually, if needed. Each of the two config objects can be used to create an ALScanViewConfig.

ALFlashConfig *cameraConfig = [[ALCameraConfig alloc] initWithJSONDictionary:cameraJSONConfig]
ALFlashConfig *flashConfig = [[ALFlashConfig alloc] initWithJSONDictionary:flashJSONConfig]

ALScanViewConfig *scanViewConfig = [[ALScanViewConfig alloc] initWithCameraConfig:cameraConfig flashConfig:flashConfig];

Tips and Best Practices

Convenience class initializers

Each Anyline Config class also has a +[withXXX:] class initializer method that you can use in place of a regular initializer.

For instance,

[ALScanViewPluginConfig withScanPluginConfig:scanPluginConfig]

is an equivalent call to

[[ALScanViewPluginConfig alloc] initWithScanPluginConfig:scanPluginConfig error:&error]

With these methods, no NSError reference parameter is required, but the functions may still potentially return null values.

Having said that, we would still recommend the version that takes an NSError parameter and to use it to handle any problems accordingly.

Use a single JSON file per use case

In most scenarios, you should find it most convenient to keep a single JSON file holding together the aggregate configuration for your scanning use case, with all component configs listed together, in your application bundle.

One example below (sample_id_config.json) describes a possible configuration for a Universal (Latin) ID use case:

sample_id_config.json
{
  "cameraConfig": {
    "captureResolution": "1080p"
  },
  "flashConfig": {
    "mode": "manual",
    "alignment": "top_right",
    "offset": { "x": 0, "y": 0 }
  },
  "viewPluginConfig": {
    "pluginConfig": {
      "id": "com.anyline.configs.plugin.id_mrz_dl",
      "universalIdConfig": {
        "allowedLayouts": {
          "mrz": [],
          "drivingLicense": [],
          "idFront": []
        },
        "alphabet": "latin",
        "drivingLicense": {
          "surname": {"scanOption": 0, "minConfidence": 40},
          "givenNames": {"scanOption": 0, "minConfidence": 40},
          "dateOfBirth": {"scanOption": 0, "minConfidence": 50},
          "placeOfBirth": {"scanOption": 1, "minConfidence": 50},
          "dateOfIssue": {"scanOption": 0, "minConfidence": 50},
          "dateOfExpiry": {"scanOption": 1, "minConfidence": 50},
          "authority": {"scanOption": 1, "minConfidence": 30},
          "documentNumber": {"scanOption": 0, "minConfidence": 40},
          "categories": {"scanOption": 1, "minConfidence": 30},
          "address": {"scanOption": 1}
        },
        "idFront": {
          "surname": {"scanOption": 0, "minConfidence": 60},
          "givenNames": {"scanOption": 0, "minConfidence": 60},
          "dateOfBirth": {"scanOption": 0, "minConfidence": 60},
          "placeOfBirth": {"scanOption": 1, "minConfidence": 60},
          "dateOfExpiry": {"scanOption": 1, "minConfidence": 60},
          "cardAccessNumber": {"scanOption": 1, "minConfidence": 60},
          "documentNumber": {"scanOption": 0, "minConfidence": 60},
          "nationality": {"scanOption": 1, "minConfidence": 60}
        }
      },
      "cancelOnResult": true
    },
    "cutoutConfig": {
      "animation": "fade",
      "maxWidthPercent": "90%",
      "maxHeightPercent": "90%",
      "width": 0,
      "alignment": "center",
      "ratioFromSize": { "width": 86, "height": 54 },
      "offset": { "x": 0, "y": 60 },
      "cropOffset": { "x": 0, "y": 0 },
      "cropPadding": { "x": 0, "y": 0 },
      "cornerRadius": 8,
      "strokeColor": "#8BE9FD",
      "strokeWidth": 2,
      "feedbackStrokeColor": "#0099FF",
      "outerColor": "#000000",
      "outerAlpha": 0.3
    },
    "scanFeedbackConfig": {
      "style": "animated_rect",
      "strokeWidth": 0,
      "strokeColor": "#8be9fd",
      "fillColor": "#908be9fd",
      "cornerRadius": 2,
      "beepOnResult": true,
      "vibrateOnResult": false,
      "blinkAnimationOnResult": false
    }
  }
}

Using this config JSON, you should be able to form the ALScanViewPlugin, ALScanPlugin and ALScanView in the following way:

// get the JSON string for the aggregate config
NSString *JSONString = [self getJSONStringForFile:@"sample_id_config"];

// NOTE: implementation found elsewhere, simplified here
CGRect frame = [self getScanViewFrame];

NSError *error;
NSDictionary *configJSONDictionary = [JSONString toJSONObject:&error];
if (!configJSONDictionary) {
    // handle the error object
}

// takes "viewPluginConfig", which covers "pluginConfig", "cutoutConfig", and "scanFeedbackConfig"
ALScanViewPlugin *scanViewPlugin = [[ALScanViewPlugin alloc] initWithJSONDictionary:configJSONDictionary error:&error];
if (!scanViewPlugin) {
    if (error) {
        NSLog(@"there was an error: %@", error.localizedDescription);
    }
    return;
}

// get the scan plugin from the scan view plugin
ALScanPlugin *scanPlugin = scanViewPlugin.scanPlugin;
scanPlugin.delegate = self;

// takes "cameraConfig" and "flashConfig" from the configJSONDictionary
ALScanViewConfig *scanViewConfig = [[ALScanViewConfig alloc] initWithJSONDictionary:configJSONDictionary error:&error];

// take scanViewPlugin, and scanViewConfig
ALScanView *scanView = [[ALScanView alloc] initWithFrame:frame
                                          scanViewPlugin:scanViewPlugin
                                          scanViewConfig:scanViewConfig
                                                   error:&error];
if (!scanView) {
    // handle the error object
}

// Next steps
// - implement the ALScanPluginDelegate
// - install the scan view into the view hierarchy
// - start the scan view camera
// - start the scan view plugin

Building components programmatically

Constructing each component programmatically takes a bit more work, but certainly is still possible. Doing this offers you the possibility to dynamically adapt your scanning use case based on your application’s workflow or user input.

Of course, you can still combine the two approaches and construct components with JSON and then adjust them afterwards in your code.

In the example below, we show how to build up such a use case for your app. Assume here that we’re working here inside your view controller:

// It is not shown here, but if an NSError is passed as a parameter make sure to
// check it after each call and handle appropriately.
NSError *error;

// configure the scan plugin (Dial Meter)
ALMeterConfig *meterConfig = [[ALMeterConfig alloc] init];
meterConfig.scanMode = [ALMeterConfigScanMode dialMeter];

ALPluginConfig *pluginConfig = [[ALPluginConfig alloc] init];
pluginConfig.meterConfig = meterConfig;
pluginConfig.identifier = @"Dial Meter";
pluginConfig.cancelOnResult = @(YES);

// create the scan plugin config
ALScanPluginConfig *scanPluginConfig = [[ALScanPluginConfig alloc] initWithPluginConfig:pluginConfig];

// create the cutout config
NSString *JSONString = [[ALCutoutConfig defaultCutoutConfig] asJSONString];
NSMutableDictionary *JSONDictionary = [NSMutableDictionary dictionaryWithDictionary:[JSONString asJSONObject]];

// configure the cutout config using a memberwise initializer
ALCutoutConfig *cutoutConfig = [[ALCutoutConfig alloc] initWithAlignment:ALCutoutAlignmentTopHalf
                                                               animation:ALCutoutAnimationStyleNone
                                                           ratioFromSize:CGSizeMake(3, 1)
                                                                  offset:CGPointZero
                                                                   width:720
                                                         maxHeightPercent:100
                                                          maxWidthPercent:100
                                                             cornerRadius:4
                                                              strokeWidth:2
                                                              strokeColor:@"0099FF"
                                                      feedbackStrokeColor:@"0099FF"
                                                               outerColor:@"30000000"
                                                               cropOffset:CGPointZero
                                                              cropPadding:CGSizeZero
                                                                    image:nil];

// create the scan feedback config using a JSON dictionary: when a value for a property is not set
// some default will be used.
JSONString = [[ALScanFeedbackConfig defaultScanFeedbackConfig] asJSONString];
JSONDictionary = [NSMutableDictionary dictionaryWithDictionary:[JSONString asJSONObject]];
JSONDictionary[@"beepOnResult"] = @(YES);
JSONDictionary[@"vibrateOnResult"] = @(YES);
ALScanFeedbackConfig *scanFeedbackConfig = [ALScanFeedbackConfig withJSONDictionary:JSONDictionary];

// assemble and create the scan view plugin config
ALScanViewPluginConfig *scanViewPluginConfig = [[ALScanViewPluginConfig alloc] initWithScanPluginConfig:scanPluginConfig
                                                                                           cutoutConfig:cutoutConfig
                                                                                     scanFeedbackConfig:scanFeedbackConfig
                                                                                                  error:&error];

// Create the scan view plugin
self.scanViewPlugin = [[ALScanViewPlugin alloc] initWithConfig:scanViewPluginConfig error:&error];

// Note that you don't need to instantiate an ALScanPlugin yourself --
// the ScanViewPlugin already creates this, and you simply access its scanPlugin property.
self.scanViewPlugin.scanPlugin.delegate = self;

// create the camera config: no customization was done, so was left as it is
ALCameraConfig *cameraConfig = [ALCameraConfig defaultCameraConfig];

// configure and create the flash config
JSONString = [[ALFlashConfig defaultFlashConfig] asJSONString];
JSONDictionary = [NSMutableDictionary dictionaryWithDictionary:[JSONString asJSONObject]];
JSONDictionary[@"alignment"] = @"bottom_right";
ALFlashConfig *flashConfig = [ALFlashConfig withJSONDictionary:JSONDictionary];

// note that you can pass null to either config here and the config defaults would be used.
ALScanViewConfig *scanViewConfig = [[ALScanViewConfig alloc] initWithCameraConfig:cameraConfig
                                                                      flashConfig:flashConfig];

self.scanView = [[ALScanView alloc] initWithFrame:CGRectZero
                                   scanViewPlugin:scanViewPlugin
                                   scanViewConfig:scanViewConfig
                                            error:&error];

// Next Steps:
// - install the scan view into the view hierarchy
// - implement ALScanPluginDelegate for this view controller
// - start the scan view camera
// - start the scan view plugin

From the code example above, you can see how config objects can be created either through memberwise initialization (see: ALCutoutConfig) or with a JSON dictionary (for instance, ALScanFeedbackConfig). Each approach can be useful in their own way.

Memberwise initialization allows you to deterministically and completely define a config object, but it requires you to provide a value for every property in the config class.

The JSON approach shown above, on the other hand, is more flexible in allowing you to provide only non-default values for your config object, but you trade away a measure of type safety in using it.

Changing the config in real time

Your work flow may require you to update the scan config while leaving the scan view camera running. To take an example, for instance, you may have a UI switch over the scan view to toggle the cutout width-height ratio (eg 3:1, 2:1, and so on), and it was tapped by the user.

In such a case, it is important to remember that once a component has been created, it can no longer be modified. For example, changing the scan mode of a plugin is not possible:

NOTE: this won’t work!
((ALScanViewPlugin *)self.scanViewPlugin).scanPlugin.scanPluginConfig.pluginConfig.meterConfig.scanMode = ALMeterConfigScanMode.dialMeter;

Instead, one should recreate the ScanViewPlugin (including ScanPlugin) with the updated configuration, and then either replace the old scan view in the view hierarchy with a new one made with the above components, or (to avoid the display momentarily showing a black background in the process) reload the existing scan view using its -setScanViewPlugin:error: method.

You can use the approach discussed in Building components programmatically as a starting point. Structuring how you assemble the components would go a long way towards making this manageable.

Below is an example of how you might for instance change the meter scan mode dynamically:

+ (ALScanViewPluginConfig *)defaultScanViewPluginConfig {
    // here we use a JSON-based config as a starting point. We will be introducing changes to it later.
    NSString *configStr = [self.class defaultJSONConfigStr];
    return [[ALScanViewPluginConfig alloc] initWithJSONDictionary:[configStr asJSONObject] error:nil];
}

+ (ALScanViewConfig *)defaultScanViewConfig {
    NSString *configStr = [self.class defaultJSONConfigStr];
    return [[ALScanViewConfig alloc] initWithJSONDictionary:[configStr asJSONObject] error:nil];
}

+ (ALScanViewPlugin *)scanViewPluginWithMeterScanMode:(ALMeterConfigScanMode *)scanMode {
    // all config properties that can change during runtime are confined within this method.
    // In this case, we want to have a scan view plugin created for a different meter scan mode than
    // the one currently running, based on for instance, the selection of the user.
    // The result is an ALScanViewPlugin that can be given to a new or existing ALScanView to
    // effect the scan mode change.
    ALScanViewPluginConfig *scanViewPluginConfig = [self.class defaultScanViewPluginConfig];
    ALMeterConfig *meterConfig = scanViewPluginConfig.scanPluginConfig.pluginConfig.meterConfig;

    meterConfig.scanMode = scanMode;

    NSError *error;
    ALScanViewPlugin *ret = [[ALScanViewPlugin alloc] initWithConfig:scanViewPluginConfig error:&error];

    // check and handle any errors

    return ret;
}

- (void)loadScanViewWithMeterScanMode:(ALMeterConfigScanMode *)newScanMode {
    ALScanViewPlugin *newScanViewPlugin = [self.class scanViewPluginWithMeterScanMode:newScanMode];

    NSError *error;
    [self.scanView setScanViewPlugin:newScanViewPlugin error:&error];
    // handle the error, if non-null
}

With this in place, you can now simply call -loadScanViewWithMeterScanMode: and update the scan process with the new meter mode whenever it is required.


Questions or need further assistance? Reach us at [email protected].