IFace.js

/**
* @module IFace
*/

import ICAO from './ICAO.js'
import ImageSource from './ImageSource.js'
import { BASE_URL, checkResponse } from './Common.js'

// This is not exported as we may eventually detect this from the plugin
// See: Iface.matchModes()
const VALID_MODES = ['accurate', 'balanced', 'fast', 'accurate_server']

/**
 * Probe
 * @classdesc Probe Class for Innovatrics IFace SDK
 *
 * @see {@link IFace}
 */
export class Probe extends ImageSource {
  /**
   * Instantiate an IFace Probe
   * @constructor
   * @param {string} source - The image source for the probe.
   * @param {string} [type] - The type of data in the image source, valid values
   * are 'url', 'base64', and 'dataurl'. If not provided 'base64' is the default.
   *
   * @example
   * const candidate = new Candidate('/person/123.jpg', '123', 'url')
   */
  constructor (source, type = 'base64') {
    super(source, type)
  }
}

/**
 * Candidate
 * @classdesc Candidate Class for Innovatrics IFace SDK
 * @extends ImageSource
 * @see {@link IFace}
 */
export class Candidate extends ImageSource {
  id

  // TODO: Support setting different modes on individual candidates
  // an example usecase for this is to set 'accurate_server' the most recent
  // photo, and 'fast' for the remaining set (since they likely have already
  // been verified at some point) In the plugin we would have to re-template the
  // probe for each additional mode and it doesn't make sense to do so until
  // the plugin is checking each photo against the probe in a separate thread.
  mode

  /**
   * Instantiate an IFace Candidate
   * @constructor
   * @param {string} source - The image source for the candidate.
   * @param {string} id - The ID for the candidate when the match results are
   * returned this Id will be returned with it's respective results.
   * @param {string} [type] - The type of data in the image source, valid values
   * are 'url', 'base64', and 'dataurl'. If not provided 'base64' is the default.
   *
   * @example
   * const candidate = new Candidate('/person/123.jpg', '123', 'url')
   */
  constructor (source, id, type = 'base64') {
    super(source, type)
    if (id) {
      this.id = id
    } else {
      throw new Error('Candidate ID not provided')
    }
  }

  toJSON () {
    return {
      id: this.id,
      base64: this.imageData
    }
  }
}

/**
 * IFace
 * @classdesc Class for Innovatrics IFace SDK
 *
 */
export class IFace extends ICAO {
  /**
   * Instantiate an IFace plugin
   * @constructor
   * @param {string} [baseURL] - Protocol, domain, and port for the service.
   *
   * @example
   * const iface = new IFace()
   */
  constructor (baseUrl = BASE_URL) {
    super('capture_verify_iface', baseUrl)
  }

  /**
   * Return list of available face matching modes
   *
   * @returns {string[]} List of available face matching modes
   *
   * @async
   * @example
   * const iface = new IFace()
   * const modes = await iface.matchModes()
   * // do something with modes, such as build a <select> element
   * const select = document.createElement('select')
   * modes.forEach(mode => {
   *   const option = document.createElement('option')
   *   option.value = mode
   *   select.appendChild(mode)
   * })
   * document.body.appendChild(select)
   */
  async matchModes () {
    // This is async as we may later get this from the plugin at runtime
    return VALID_MODES
  }

  /**
   * Check the ICAO compliance of an image
   * @param {string} image - A base64 encoded image.
   *
   * @param {string|bool} [crop] - If falsy image will not be cropped, otherwise
   * the value will be interpreted as the preferred crop method which will vary
   * by plugin implementation.
   *
   * @param {string} [background=#ffffff] - Hexadecimal color that will fill in
   * any part of the cropped image that falls outside the original source image.
   *
   * @returns {object} Results of the ICAO check, this may vary by plugin
   * implementation.
   *
   * @async
   * @example
   * // Take a photo with a Canon Camera and perform ICAO checks on the
   * // returned image
   * const camera = new CanonCamera()
   * const photo = await camera.takePhoto('base64')
   * const iface = new IFace()
   * const results = await iface.icao(photo)
   * const img = document.createElement('img')
   * img.src = 'data:image/jpeg;base64,' + results.cropped
   * console.log(`found ${result.faces_found} faces in the image`)
   * document.body.appendChild(img)
   */
  async icao (image, cropMethod = false, cropBackground) {
    return await this.check(image, cropMethod, cropBackground)
  }

  /**
   * Perform a facial match against one or more photos.
   *
   * @param {object|string} probe - Either a Probe object or a base64 encoded
   * image that is being compared or searched against one or more candidates
   *
   * @param {object[]} candidates - An array of candidate objects against which
   * the probe photo is compared.
   * @param {string} candidates[].id - Id of the photo, when the results of the
   * matched are returned, this id will be returned so results can be matched
   * back to their original image. Must be less than 16 characters.
   * @param {string} candidates[].base64 - Base64 encoded string containing the
   * photo.
   * @param {string} [candidates[].mode] - Matching mode to use for this photo.
   * If left unspecified will use the mode provided in the mode parameter.
   *
   * @param {string} [mode=fast] - Matching mode to use for all images, can be
   * overriden on each candidate if desired. Valid values are: 'accurate',
   * 'balanced', 'fast', and 'accurate_server'.
   *
   * @async
   * @example
   * // Create an iface object
   * const iface = new IFace()
   *
   * // Obtain a photo for the probe photo
   * const probe = await camera.takePhoto('base64')
   *
   * // Create a candidate set from the person's previous photos
   * const candidates = [
   *   new Candidate('/person/1/photo/2.jpg', '3', 'url'),
   *   new Candidate('/person/1/photo/1.jpg', '2', 'url')
   * ]
   *
   * // Match the probe to all the candidates using the 'balanced' mode
   * const results = await iface.match(probe, candidates, 'balanced')
   *
   * // use the results
   * console.log(results)
   */
  async match (probe, candidates, mode = 'fast') {
    if (VALID_MODES.indexOf(mode) === -1) {
      throw new Error('Invalid mode provided')
    }

    // If this is a Probe object fetch it's image data
    if (typeof probe === 'object' && typeof probe.data === 'function') {
      await probe.data()
    }

    // And fetch data for candidates
    const asyncOperations = candidates.map(async candidate => {
      if (typeof candidate === 'object' &&
        typeof candidate.data === 'function') {
        await candidate.data()
      }
    })

    // Wait for all async operations to complete
    await Promise.all(asyncOperations)

    const body = {
      probe,
      mode,
      candidates
    }
    const options = {
      method: 'POST',
      mode: 'cors',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    }
    const url = `${this.baseUrl}/plugin/${this.id}/face/match`
    const response = await fetch(url, options)
    await checkResponse(response)
    return await response.json()
  }
}

export default IFace