OCR with Dynamics 365 - Part 2: Connect to Azure Form Recognizer through JavaScript web-resource

Ruben Aghajanyan
 on Thu Mar 02 2023

Introduction

Microsoft Azure Form Recognizer is an AI service that uses Optical Character Recognition (OCR) to extract text and structure from documents and images. It is quite exciting to easily integrate Microsoft Azure Form Recognizer with Dynamics 365.

Here is how the end result will look.

0

In this article, we will see:

  • How to call Azure Form Recognizer API from JavaScript.
  • How to get recognized tag values.
  • How to get recognized table tag values.

Html component

We will create an Html web resource, to drag and drop the invoice .jpg file on the control. Then we will use ondrop event to call OCR API and get back extracted data from the file.

Create the following Html web resource and put it in the invoice form (In this example I'm using custom Invoice and Invoice Item entities).

<html>
<head>
  <style>
    .container {
      width: 100%;
      height: auto;
    }

    .dropArea {
      width: 100%;
      height: auto;
      border: solid 2px rgba(93, 104, 105, 0.62);
      padding: 25px 0px;
      transition: background-color .5s, padding .5s;
      font-size: 20px;
      color: black;
    }

    .textIcon {
      width: 100%;
    }

    .text {
      width: 100%;
      text-align: center;
      font-family: sans-serif;
      transition: font-size .5s, color .5s;
    }

    .dropAreaHover {
      background-color: #c0bebe54;
      font-size: 25px;
      padding: 23px 0px 23px;
      color: #939393;
    }
  </style>
</head>
<body style="overflow-wrap: break-word;">
  <meta charset="utf-8"> <title></title> <div class="container" style="font-family: undefined;"> <div class="dropArea"> <div class="textIcon"> <div class="text"> <p>Drag and drop files here to upload</p> </div> </div> </div> </div>
  <script>
  </script>
</body>
</html>

Event handlers

Let's add the following event handlers.

<script>
  window.onload = function () {
    const dropArea = document.getElementsByClassName('dropArea')[0];
    ['dragover', 'dragleave', 'drop'].forEach(eventName => {
      document.addEventListener(eventName, event => {
        event.preventDefault();
      });
    });

    ['dragover'].forEach(eventName => {
      dropArea.addEventListener(eventName, () => {
        dropArea.classList.add('dropAreaHover');
      });
    });

    ['drop'].forEach(eventName => {
      dropArea.addEventListener(eventName, event => {
        const dt = event.dataTransfer;
        const { files } = dt;
        setOCRValues(files[0]);
        dropArea.classList.remove('dropAreaHover');
      });
    });

    ['dragleave'].forEach(eventName => {
      dropArea.addEventListener(eventName, () => {
        dropArea.classList.remove('dropAreaHover');
      });
    });
  };
</script>

JavaScript functions

The "analyzeDocument" function is used to send the image file to the Form Recognizer API and get back the extracted data.

async function analyzeDocument(file) {
  const endpoint = ''; // Specify form recognizer url
  const apiKey = ''; // Specify form recognizer key 1
  const modelId = ''; // Specify form recognizer model id
  const analyzeUrl = `${endpoint}/formrecognizer/v2.1/custom/models/${modelId}/analyze`;
  const response = await fetch(analyzeUrl, {
    method: 'POST',
    body: file,
    headers: { 'Ocp-Apim-Subscription-Key': `${apiKey}` },
  });
  const analyzeResultUrl = response.headers.get('Operation-Location');
  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      try {
        const analyzeResultResponse = await fetch(analyzeResultUrl, {
          method: 'GET',
          headers: { 'Ocp-Apim-Subscription-Key': `${apiKey}` },
        });
        const content = await analyzeResultResponse.json();
        if (content.status === 'succeeded') {
          clearInterval(interval);
          resolve(content.analyzeResult);
        }
      }
      catch (error) {
        console.log(`Error occured while trying to analyze the file. ${error.message}`);
        clearInterval(interval);
        reject(error);
      }
    }, 1000);
  });
}

The "setOCRValues" function will be triggered from the ondrop event. It's used to send a file to "analyzeDocument" function and set extracted data in the form fields.

async function setOCRValues(file) {
  const analyzeResult = await analyzeDocument(file);
  const { fields } = analyzeResult.documentResults[0];
  let invoiceLineRecords = null;
  // Invoice Number is a tag name from Azure form recognizer
  parent.Xrm.Page.getAttribute('bvr_name').setValue(fields['Invoice Number'].valueString);
  // Invoice Date is a tag name from Azure form recognizer
  parent.Xrm.Page.getAttribute('bvr_invoice_date').setValue(fields['Invoice Date'].valueString);
  parent.Xrm.Page.data.save();
}

We need also to get values of the "Invoice Lines" table tag values. Let's first modify the "setOCRValues" function to get table tag values. Next, let's use the setInterval() function to periodically check if the form is saved, and if yes we will call "processInvoiceLine" function to create Invoice Items.

async function setOCRValues(file) {
  const analyzeResult = await analyzeDocument(file);
  const { fields } = analyzeResult.documentResults[0];
  let invoiceLineRecords = null;
  parent.Xrm.Page.getAttribute('bvr_name').setValue(fields['Invoice Number'].valueString);
  parent.Xrm.Page.getAttribute('bvr_invoice_date').setValue(fields['Invoice Date'].valueString);
  // Invoice Lines is a table tag name from Azure form recognizer.
  invoiceLineRecords = fields['Invoice Lines'].valueArray.map(obj => obj.valueObject);
  parent.Xrm.Page.data.save();
  const interval = setInterval(() => {
    const invoiceId = parent.Xrm.Page.data.entity.getId();
    if (invoiceId !== '' && processInvoiceLine !== undefined) {
      clearInterval(interval);
      processInvoiceLine(invoiceLineRecords);
    }
  }, 100);
}

And let's add "processInvoiceLine" and "createInvoiceLine" functions, which are responsible for Invoice Items creation.

async function processInvoiceLine(invoiceLineRecords) {
  const invoiceId = parent.Xrm.Page.data.entity.getId().toLowerCase().replace(/[{}]/g, '');
  for (const record of invoiceLineRecords) {                    
    let description = record['Description'].valueString;
    let quantity = record['Quantity'].valueString;
    let pricePerUnit = record['Unit Price'].valueString;
    await createInvoiceLine(invoiceId, description, quantity, pricePerUnit);
    parent.Xrm.Page.getControl('invoice_items').refresh();
  }
}
async function createInvoiceLine(invoiceId, description, quantity, pricePerUnit) {
  const data = {
    'bvr_invoice@odata.bind': `/bvr_invoices(${invoiceId})`,
    'bvr_description': description,
    'bvr_quantity': quantity,
    'bvr_unit_price': pricePerUnit,
  };
  await parent.Xrm.WebApi.createRecord('bvr_invoice_item', data);
}

Below is the full JavaScript code.

window.onload = function () {
  const dropArea = document.getElementsByClassName('dropArea')[0];
  ['dragover', 'dragleave', 'drop'].forEach(eventName => {
    document.addEventListener(eventName, event => {
      event.preventDefault();
    });
  });

  ['dragover'].forEach(eventName => {
    dropArea.addEventListener(eventName, () => {
      dropArea.classList.add('dropAreaHover');
    });
  });

  ['drop'].forEach(eventName => {
    dropArea.addEventListener(eventName, event => {
      const dt = event.dataTransfer;
      const { files } = dt;
      setOCRValues(files[0]);
      dropArea.classList.remove('dropAreaHover');
    });
  });

  ['dragleave'].forEach(eventName => {
    dropArea.addEventListener(eventName, () => {
      dropArea.classList.remove('dropAreaHover');
    });
  });

  async function setOCRValues(file) {
    const analyzeResult = await analyzeDocument(file);
    const { fields } = analyzeResult.documentResults[0];
    let invoiceLineRecords = null;
    parent.Xrm.Page.getAttribute('bvr_name').setValue(fields['Invoice Number'].valueString);
    parent.Xrm.Page.getAttribute('bvr_invoice_date').setValue(fields['Invoice Date'].valueString);
    invoiceLineRecords = fields['Invoice Lines'].valueArray.map(obj => obj.valueObject);
    parent.Xrm.Page.data.save();
    const interval = setInterval(() => {
      const invoiceId = parent.Xrm.Page.data.entity.getId();
      if (invoiceId !== '' && processInvoiceLine !== undefined) {
        clearInterval(interval);
        processInvoiceLine(invoiceLineRecords);
      }
    }, 100);
  }

  async function processInvoiceLine(invoiceLineRecords) {
    const invoiceId = parent.Xrm.Page.data.entity.getId().toLowerCase().replace(/[{}]/g, '');
    for (const record of invoiceLineRecords) {                    
      let description = record['Description'].valueString;
      let quantity = record['Quantity'].valueString;
      let pricePerUnit = record['Unit Price'].valueString;
      await createInvoiceLine(invoiceId, description, quantity, pricePerUnit);
      parent.Xrm.Page.getControl('invoice_items').refresh();
    }
  }

  async function createInvoiceLine(invoiceId, description, quantity, pricePerUnit) {
    const data = {
      'bvr_invoice@odata.bind': `/bvr_invoices(${invoiceId})`,
      'bvr_description': description,
      'bvr_quantity': quantity,
      'bvr_unit_price': pricePerUnit,
    };
    await parent.Xrm.WebApi.createRecord('bvr_invoice_item', data);
  }

  async function analyzeDocument(file) {
    const endpoint = ''; // Specify form recognizer url
    const apiKey = ''; // Specify form recognizer key 1
    const modelId = ''; // Specify form recognizer model id
    const analyzeUrl = `${endpoint}/formrecognizer/v2.1/custom/models/${modelId}/analyze`;
    const response = await fetch(analyzeUrl, {
      method: 'POST',
      body: file,
      headers: { 'Ocp-Apim-Subscription-Key': `${apiKey}` },
    });
    const analyzeResultUrl = response.headers.get('Operation-Location');
    return new Promise((resolve, reject) => {
      const interval = setInterval(async () => {
        try {
          const analyzeResultResponse = await fetch(analyzeResultUrl, {
            method: 'GET',
            headers: { 'Ocp-Apim-Subscription-Key': `${apiKey}` },
          });
          const content = await analyzeResultResponse.json();
          if (content.status === 'succeeded') {
            clearInterval(interval);
            resolve(content.analyzeResult);
          }
        }
        catch (error) {
          console.log(`Error occured while trying to analyze the file. ${error.message}`);
          clearInterval(interval);
          reject(error);
        }
      }, 1000);
    });
  }
};

This is the last part of my blog posts on Dynamics 365 and Azure form recognizer. Hope you like it!