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:
Organizing your local machine and app instance using prerequisites
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)
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)
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:
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:
Online License token file must be located in Bin64 folder.
Create an Azure Storage account (frestorage in this article). All needed instructions you can find on Azure website.
Create your App Service as desired (see instructions here).
Create two Blob containers inside frestorage:
fre-lib - for the ABBYY FineReader Engine files
processing-container - for processing results
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).
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).
Create two queues inside frestorage:
processing-queue - for setting the tasks of files processing
status-queue - for notifying about task completion
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:
Publish FreDeployerJob to Azure App Service using Visual Studio (set Triggered for WebJob Type).
Open your App Service in the Azure portal.
Open WebJobs of your App Service.
Find FreDeployerJob in the list of WebJobs.
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:
Upload the file you intend to process to the processing-container.
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"}
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.
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.
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";
Functions.cs
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."); } } } } }}
Program.cs
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(); } }}
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; } }}
Functions.cs
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()); } } }}
Program.cs
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(); } }}
EngineLoader.cs
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; }}
IFileWriter.cs
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; }}
Processor.cs
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(); } }}