Skip to main content
This section provides instructions on how to deploy an FRE 12 for Windows application to Azure App Service. As an example, a pair of WebJob projects that use data from Azure Storage account is represented. Files processing is performed using a Blob container.
Using this scenario, you will obtain the best recognition results for small one-page documents, such as invoices, receipts, etc.
Deploying your application to App Service includes several steps:
  1. Organizing your local machine and app instance using prerequisites
  2. Performing the preparatory steps before deploying your application
  3. Deploying and running your application in App Service
Use the code samples represented in instruction below.

Prerequisites

Local machine

Before creating your App Service, use the following specification to organize your local machine:
  • Visual Studio 2019 and its modules for developing applications for Azure (check Azure feature in Visual Studio or use Visual Studio Installer to download such modules)
  • Azure SDK (download here)
  • .NET Framework 4.7.2
  • NuGet Package for working with Azure Storage and Blob containers:
    • Azure.Storage.Blobs (download here)
    • Azure.Storage.Queues (download here)
    • Newtonsoft.Json (download here)
    • System.IO.Compression.ZipFile (download here)
  • ABBYY FineReader Engine wrapper for .Net Framework 4.7 (in the C:\ProgramData\ABBYY\SDK\12\FineReader Engine\Inc\.NET interops folder after developer installation)
  • IFileWriter interface overridden for working with the Blob containers (see the sample below)
  • Azure Storage Explorer (optional - download here)
  • Virtual machine in your Azure account intended for the licensing procedures. For the further configuration, you need:
    • IP-address
    • Open connection port (default or user-defined). To open it, use a firewall
    • Sockets network protocol connection details

Preparatory steps

Preparatory steps are to be done on your local machine. By completing these steps, you will prepare all necessary settings and files to start deploying your application:
  1. Create an archive with the ABBYY FineReader Engine Library (for example, LibraryPackage.zip). List of files represents in the FREngineDistribution.csv file.
    Important! If you have limited storage space (for example, you use an App Service Plan with 1GB space), we recommend using the /extract option to create your custom ABBYY FineReader Engine package with minimal size. The rest of the storage space will be used for processing the files.
    When creating an archive, take into account the ABBYY FineReader Engine licensing settings must be set up according to the virtual machine settings:
    • The LicensingSettings.xml file must be set up for the Network configuration (see Working with the LicensingSettings.xml File).
    • The Sockets network protocol must be used.
    • Online License token file must be located in Bin64 folder.
  2. Create an Azure Storage account (frestorage in this article). All needed instructions you can find on Azure website.
  3. Create your App Service as desired (see instructions here).
  4. Create two Blob containers inside frestorage:
    • fre-lib - for the ABBYY FineReader Engine files
    • processing-container - for processing results
  5. Upload LibraryPackage.zip to the fre-lib container in the most convenient way (using .NET, Powershell, Python script or Azure Storage Explorer/Azure Portal applications).
  6. Deploy and configure the virtual machine with the licensing settings in your Azure account:
    • Install the License Manager utility via installLM.exe from the LibraryPackage.zip.
    • Set up the Sockets network protocol in the LicensingSettings.xml, and then restart the Licensing Service.
    • Ensure that Azure App Service can access the Licensing Service connection port (adjust the Windows Firewall rules on virtual machine).
    • Activate your license (for software protection only; online protection does not require activation).
  7. Create two queues inside frestorage:
    • processing-queue - for setting the tasks of files processing
    • status-queue - for notifying about task completion
  8. Create two Azure WebJob (.NET Framework) projects in Visual Studio 2019 to work with frestorage:
    • FreDeployerJob - for deploying LibraryPackage.zip to App Service (see listing of its files: Config.cs, Functions.cs, Program.cs below)
    • FreProcessorJob - for document processing (see listing of its files: Config.cs, Functions.cs, Program.cs, EngineLoader.cs, IFileWriter.cs, Processor.cs below)

Deploying and running ABBYY FineReader Engine in App Service

To deploy ABBYY FineReader Engine:
  1. Publish FreDeployerJob to Azure App Service using Visual Studio (set Triggered for WebJob Type).
  2. Open your App Service in the Azure portal.
  3. Open WebJobs of your App Service.
  4. Find FreDeployerJob in the list of WebJobs.
  5. Launch FreDeployerJob by the right-clicking+Run command on the WebJobs tab.
You may access the Logs tab to check the result of deployment. If it succeeds, LibraryPackage.zip is uploaded from the fre-lib container and deployed inside the %HOME_EXPANDED% folder available for all entities in App Service. To deploy FreProcessorJob, publish FreProcessorJob to Azure App Service using Visual Studio (set Continuous for WebJob Type). As a result, FreProcessorJob will be in the list of WebJobs tabs of your App Service. To process a file:
  1. Upload the file you intend to process to the processing-container.
  2. Add a JSON message for a new task of processing in format {“blob-item-name” : “file_name”} to the processing-queue. If you upload Demo.tif to the processing-container, your message should be:
{"blob-item-name" : "Demo.tif"}
  1. Wait for the task to complete. As soon as the new task is set, FreProcessorJob starts to process the specified file in memory. The status-queue will contain entries about the execution of this task.
  2. Find the output file in the processing-container.
1. FreProcessorJob operates as a single-threaded process. If you intend to process your files in parallel, you need to create several FreProcessorJob that will be listening to the same queue. 2. Every additional FreProcessorJob consumes extra memory. Take into account this fact when buying your Service Plan. For example, in Azure Free Service Plan is nice to have only one FreProcessorJob that consumes a little memory and thus ensures the stability of file processing. 3. Using single FreProcessorJob is not suitable for processing large multi-page documents. In this case, consider recognition of your document in Azure Cloud Service or Azure Virtual Machine instead of App Service.

Code Samples

This section includes code samples used to deploy and implement the ABBYY FineReader Engine API in App Service.

FreDeployerJob:

using System.IO;
class Config
{
    // Connecting string to blob container
    public static readonly string ConnectionString = "your_connection_string";
    // HOME_EXPANDED directory is common for all WebJobs folder
    public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE");
    // Input and output containers name in your storage
    public static readonly string LibraryContainerName = "fre-lib";
namespace FreDeployerJob
{
    public class Functions
    {
        // This function won't be triggered automatically - you should do it manually
        [NoAutomaticTrigger]
        [Timeout("01:00:00")]
        public static void DeployFRE()
        {
            Console.WriteLine("Deploying FRE");
            // Connecting to existing input container <InputContainerName> via Storage Account connection string
            BlobContainerClient inputContainerClient = new BlobContainerClient(Config.ConnectionString, Config.LibraryContainerName);
            // Creating library directory as well as AppData and Temp folders for ABBYY FineReader Engine initialization
            if (Directory.Exists(Config.LibraryFolder) == true)
            {
                Directory.Delete(Config.LibraryFolder, true);
            }
            Directory.CreateDirectory(Config.LibraryFolder);
            Directory.CreateDirectory(Path.Combine(Config.LibraryFolder, "Temp"));
            Directory.CreateDirectory(Path.Combine(Config.LibraryFolder, "AppData"));
            // Iterating via blobs in container. Blob in <InputContainerName> is equal to some image file
            foreach (BlobItem blobItem in inputContainerClient.GetBlobs())
            {
                Console.WriteLine("\t" + blobItem.Name);
                // Searching for light version of the ABBYY FineReader Engine library
                if (blobItem.Name == "LibraryPackage.zip")
                {
                    Console.WriteLine("LibraryPackage.zip was found.");
                    // Connecting to blob to access its contents
                    BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.LibraryContainerName, blobItem.Name);
                    Console.WriteLine("Downloading to memory...");
                    // Download zip to memory
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        blobClient.DownloadTo(memoryStream);
                        Console.WriteLine("LibraryPackage.zip was downloaded.");
                        Console.WriteLine("Unzipping...");
                        // Unzip without using temp folder (only memory processing)
                        using (ZipArchive archive = new ZipArchive(memoryStream))
                        {
                            foreach (ZipArchiveEntry entry in archive.Entries)
                            {
                                string subDirectory = Path.GetDirectoryName(Path.Combine(Config.LibraryFolder, entry.FullName));
                                if (Directory.Exists(subDirectory) == false)
                                {
                                    Directory.CreateDirectory(subDirectory);
                                }
                                if (entry.Name.Length != 0)
                                {
                                    entry.ExtractToFile(Path.Combine(subDirectory, entry.Name));
                                }
                            }
                        }
                        Console.WriteLine("LibraryPackage.zip was unzipped to HOME_EXPANDED.");
                    }
                }
            }
        }
    }
}
namespace FreDeployerJob
{
    // To learn more about Microsoft Azure WebJobs SDK, please see https://go.microsoft.com/fwlink/?LinkID=320976
    class Program
    {
        // Please set the following connection strings in app.config for these WebJobs to run:
        // AzureWebJobsDashboard and AzureWebJobsStorage
        static void Main()
        {
            // When triggered this WebJob will only call this method
            Functions.DeployFRE();
        }
    }
}

FreProcessorJob:

using System;
using System.IO;
namespace FreProcessorJob
{
    class Config
    {
        // Connecting string to blob container
        public static readonly string ConnectionString = "your_connection_string";
        // HOME_EXPANDED directory is common for all WebJobs folders
        // It is the same as in FreDeployerJob project
        public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE");
        // Processing container name in your storage
        public static readonly string ProcessingContainerName = "processing-container";
        // Processing queue name
        public static readonly string ProcessingQueueName = "processing-queue";
        public static readonly string StatusQueueName = "status-queue";
        // Return Customer Project ID for ABBYY FineReader Engine
        public static String GetCustomerProjectId()
        {
            return "your_cpid";
        }
 
        // Return name of online license token
        // If you don't use online license, leave an empty string
        // Token should be located in Bin64 folder of library package
        public static String GetLicenseTokenName()
        {
            return "your_online_license_token_if_you_have_it";
        }
 
        // Return online license password
        // If you don't use online license, leave an empty string
        public static String GetLicensePassword()
        {
            return "online_license_password_if_you have_it";
        }
 
        // Return name of license token
        // Licensing is located in Bin64 folder of the library package
        public static String GetLicenseTokenName()
        {
            return "your_licence_for_ABBYY FineReader Engine";
        }
 
        // Return license password
        public static String GetLicensePassword()
        {
            return "license_password";
        }
 
        // Return engine path
        public static String GetEngineFolder()
        {
            string engineSubfolder = "Bin64";
            string engineDllFolder = Path.Combine(LibraryFolder, engineSubfolder);
            return engineDllFolder;
        }
    }
}
using Microsoft.Azure.WebJobs;
using System;
using System.IO;
using Azure.Storage.Blobs;
using Azure.Storage.Queues;
using Newtonsoft.Json.Linq;
namespace FreProcessorJob
{
    public class Functions
    {
        // This function will get triggered/executed when a new message is written
        // on an Azure Queue called processing-queue
        // The message is expected to be a JSON message with 'blob-item-name' key
        // processing results will be saved to processing container
        // processing status will be sent to status-queue in JSON format
        public static void ProcessQueueMessage([QueueTrigger("processing-queue")] string message)
        {
            // First of all, connecting to status-queue
            QueueClient queueClient = new QueueClient(Config.ConnectionString, Config.StatusQueueName);
            try
            {
                // This will be logged to WebJob logs on Azure portal
                Console.WriteLine("Accepted task: " + message);
                JObject task = JObject.Parse(message);
                task["processor_id"] = Environment.GetEnvironmentVariable("WEBJOBS_NAME");
 
                // This will be send to status-queue
                task["status"] = "accepted";
                queueClient.SendMessage(task.ToString());
 
                // Getting blob-item-name - the name of file in Processing container
                string blobFileName = task["blob-item-name"].ToString();
                BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, blobFileName);
                // Loading blob into memory
                Console.WriteLine("\t Downloading blob to memory: " + blobFileName);
                MemoryStream memoryStream = new MemoryStream();
                blobClient.DownloadTo(memoryStream);
                Console.WriteLine("\t Downloaded.");
                // Updating status to processing
                Console.WriteLine("\t Processing in FRE: " + blobFileName);
                task["status"] = "processing";
                queueClient.SendMessage(task.ToString());
                // Processing the downloaded blob in ABBYY FineReader Engine using memory processing methods
                // The output is the name of processing result saved as blob in <ProcessingContainerName>
                string resultBlobName = "";
                using (FreProcessor.Processor freProcessor = new FreProcessor.Processor())
                {
                    resultBlobName = freProcessor.ProcessBlobFromMemory(memoryStream, blobFileName);
                    Console.WriteLine("\t Result blob name in output container: " + resultBlobName);
                }
                // Deleting input image
                Console.WriteLine("\t Deleting from input container: " + blobFileName);
                blobClient.Delete();
                // Updating status to succeeded
                // in Azure portal logs
                Console.WriteLine("Succeeded");
 
                // in status-queue
                task["status"] = "succeeded";
                task["result-blob-name"] = resultBlobName;
                queueClient.SendMessage(task.ToString());
            }
            catch (Exception error)
            {
                // In case of any errors reporting
                // to Azure portal logs
                Console.WriteLine("Failed: " + error.Message);
                // to status-queue
                JObject task = new JObject(); 
                task["processor_id"] = Environment.GetEnvironmentVariable("WEBJOBS_NAME");
                task["status"] = "failed";
                task["error"] = error.Message;
                task["task"] = message;
                queueClient.SendMessage(task.ToString());
            }
        }
    }
}
using Microsoft.Azure.WebJobs;
namespace FreProcessorJob
{
    // To learn more about Microsoft Azure WebJobs SDK, please see https://go.microsoft.com/fwlink/?LinkID=320976
    class Program
    {
        // Please set the following connection strings in app.config for these WebJobs to run:
        // AzureWebJobsDashboard and AzureWebJobsStorage
        static void Main()
        {
            var config = new JobHostConfiguration();
            if (config.IsDevelopment)
            {
                config.UseDevelopmentSettings();
            }
            // ABBYY FineReader Engine is not thread-safe, so we cannot process more than one message simultaneously
            config.Queues.BatchSize = 1;
            var host = new JobHost(config);
            // The following code ensures that the WebJob will be running continuously
            // as one of functions is attached to Azure queue and listens for new tasks
            host.RunAndBlock();
        }
    }
}
using System;
using System.IO;
using System.Runtime.InteropServices;
using FREngine;
namespace FreProcessorJob.FreProcessor
{
    // Class for loading/unloading FREngine.dll and initializing/deinitializing Engine
    // Loading is performed in constructor, unloading in Dispose()
    // Throws exceptions when loading fails
    public class EngineLoader : IDisposable
    {
        // Load ABBYY FineReader Engine with settings stored in SamplesConfig.cs
        public EngineLoader()
        {
            string enginePath = Path.Combine(Config.GetEngineFolder(), "FREngine.dll");
            string customerProjectId = Config.GetCustomerProjectId();
            string licensePath = Path.Combine(Config.GetEngineFolder(), Config.GetLicenseTokenName());
            string licensePassword = Config.GetLicensePassword();
            try
            {
                // Load the FREngine.dll library
                dllHandle = LoadLibraryEx(enginePath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH);
                if (dllHandle == IntPtr.Zero)
                {
                    int error = Marshal.GetLastWin32Error();
                    Console.WriteLine("The last Win32 Error was: " + error);
                    throw new Exception("Can't load " + enginePath);
                }
 
                IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine");
                if (initializeEnginePtr == IntPtr.Zero)
                {
                    throw new Exception("Can't find InitializeEngine function");
                }
                IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine");
                if (deinitializeEnginePtr == IntPtr.Zero)
                {
                    throw new Exception("Can't find DeinitializeEngine function");
                }
                IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow");
                if (dllCanUnloadNowPtr == IntPtr.Zero)
                {
                    throw new Exception("Can't find DllCanUnloadNow function");
                }
                // Convert pointers to delegates
                initializeEngine = (InitializeEngine)Marshal.GetDelegateForFunctionPointer(
                    initializeEnginePtr, typeof(InitializeEngine));
                deinitializeEngine = (DeinitializeEngine)Marshal.GetDelegateForFunctionPointer(
                    deinitializeEnginePtr, typeof(DeinitializeEngine));
                dllCanUnloadNow = (DllCanUnloadNow)Marshal.GetDelegateForFunctionPointer(
                    dllCanUnloadNowPtr, typeof(DllCanUnloadNow));
                // Call the InitializeEngine function
                string dataFolder = Path.Combine(Config.LibraryFolder, "AppData");
                string tempFolder = Path.Combine(Config.LibraryFolder, "Temp");
                int hresult = initializeEngine(customerProjectId, licensePath, licensePassword,
                    dataFolder, tempFolder, false, ref engine);
                Marshal.ThrowExceptionForHR(hresult);
            }
            catch (Exception)
            {
                // Free the FREngine.dll library
                engine = null;
                // Deleting all objects before FreeLibrary call
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                FreeLibrary(dllHandle);
                dllHandle = IntPtr.Zero;
                initializeEngine = null;
                deinitializeEngine = null;
                dllCanUnloadNow = null;
                throw;
            }
        }
        // Unload ABBYY FineReader Engine
        public void Dispose()
        {
            if (engine == null)
            {
                // Engine was not loaded
                return;
            }
            engine = null;
            int hresult = deinitializeEngine();
            // Deleting all objects before FreeLibrary call
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            hresult = dllCanUnloadNow();
            if (hresult == 0)
            {
                FreeLibrary(dllHandle);
            }
            dllHandle = IntPtr.Zero;
            initializeEngine = null;
            deinitializeEngine = null;
            dllCanUnloadNow = null;
            // throwing exception after cleaning up
            Marshal.ThrowExceptionForHR(hresult);
        }
        // Returns pointer to ABBYY FineReader Engine's main object
        public IEngine Engine
        {
            get
            {
                return engine;
            }
        }
        // Kernel32.dll functions
        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibraryEx(string dllToLoad, IntPtr reserved, uint flags);
        private const uint LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008;
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
        [DllImport("kernel32.dll")]
        private static extern bool FreeLibrary(IntPtr hModule);
        // FREngine.dll functions
        [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
        private delegate int InitializeEngine(string customerProjectId, string licensePath, string licensePassword,
            string dataFolder, string tempFolder, bool isSharedCPUCoresMode, ref FREngine.IEngine engine);
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        private delegate int DeinitializeEngine();
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        private delegate int DllCanUnloadNow();
        // private variables
        private FREngine.IEngine engine = null;
        // Handle to FREngine.dll
        private IntPtr dllHandle = IntPtr.Zero;
        private InitializeEngine initializeEngine = null;
        private DeinitializeEngine deinitializeEngine = null;
        private DllCanUnloadNow dllCanUnloadNow = null;
    }
}
using System;
using System.IO;
using Azure.Storage.Blobs;
namespace FreProcessorJob.FreProcessor
{
    public class FileWriter : FREngine.IFileWriter, IDisposable
    {
        public FileWriter(string _resultBlobName, string _fileExtension)
        {
            resultBlobName = _resultBlobName;
            fileExtension = _fileExtension;
        }
        public void Open(string fileName, ref int bufferSize)
        {
            stream = new MemoryStream();
        }
        public void Write(byte[] data)
        {
            stream.Write(data, 0, data.Length);
        }
        public void Close()
        {
            // Creating connection to new blob in <ProcessingContainerName>. The processing result will be stored there
            BlobClient resultBlobClient = new BlobClient(Config.ConnectionString, 
                Config.ProcessingContainerName,
                resultBlobName + fileExtension);
            // Rewrite existing file
            resultBlobClient.DeleteIfExists();
            // Setting position to 0 to write file from beginning
            stream.Position = 0;
            resultBlobClient.Upload(stream);
            stream.Close();
        }
        public void Dispose()
        {
            // Closing memory stream on disposal to be able to access it after data was written
            stream.Close();
        }
        private string resultBlobName;
        private string fileExtension;
        private MemoryStream stream;
    }
}
using System;
using System.Runtime.InteropServices;
using System.IO;
using FREngine;
namespace FreProcessorJob.FreProcessor
{
    class Processor : IDisposable
    {
        private EngineLoader engineLoader = null;
        private void displayMessage(string text)
        {
            Console.WriteLine("\t" + text);
        }
        private void setupFREngine()
        {
            displayMessage("Loading predefined profile...");
            // this is optional
            engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy");
            // this is mandatory on low-performing App Service Plans as we will be getting errors on parallel processing
            engineLoader.Engine.MultiProcessingParams.MultiProcessingMode = MultiProcessingModeEnum.MPM_Sequential;
        }
        private void LoadEngine()
        {
            try
            {
                if (engineLoader == null)
                {
                    engineLoader = new EngineLoader();
                }
                setupFREngine();
            }
            catch (Exception error)
            {
                displayMessage("error: " + error.Message);
            }
        }
        private void UnloadEngine()
        {
            try
            {
                if (engineLoader != null)
                {
                    engineLoader.Dispose();
                    engineLoader = null;
                }
            }
            catch (Exception error)
            {
                displayMessage("error: " + error.Message);
            }
        }
        public Processor()
        {
            LoadEngine();
        }
        public string ProcessBlobFromMemory(MemoryStream inputMemoryStream, string inputBlobName)
        {
            FRDocument document = engineLoader.Engine.CreateFRDocument();
            string resultBlobName = "";
            try
            {
                document.PageFlushingPolicy = FREngine.PageFlushingPolicyEnum.PFP_KeepInMemory;
 
                // Add image file to document
                displayMessage("Loading image...");
                IntPtr handle = Marshal.AllocHGlobal(inputMemoryStream.GetBuffer().Length);
                Marshal.Copy(inputMemoryStream.GetBuffer(), 0, handle, inputMemoryStream.GetBuffer().Length);
                document.AddImageFileFromMemory(handle.ToInt64(), null, null);
                // Recognize the document
                displayMessage("Recognizing...");
                document.Process(null);
                // Save results
                displayMessage("Saving results...");
                FileWriter fileWriter = new FileWriter(inputBlobName, ".pdf");
                resultBlobName = inputBlobName + ".pdf";
                document.ExportToMemory(fileWriter, FREngine.FileExportFormatEnum.FEF_PDF, null);
            }
            catch (Exception error)
            {
                displayMessage("error: " + error.Message);
                throw error;
            }
            finally
            {
                // Close the document
                document.Close();
            }
            return resultBlobName;
        }
        public void Dispose()
        {
            UnloadEngine();
        }
    }
}