Use this file to discover all available pages before exploring further.
Cette section fournit des instructions pour déployer une application FRE 12 pour Windows dans Azure App Service. À titre d’exemple, deux projets WebJob utilisant des données d’un compte de stockage Azure sont présentés. Le traitement des fichiers est effectué à l’aide d’un conteneur Blob. Avec ce scénario, vous obtiendrez les meilleurs résultats de reconnaissance pour de petits documents d’une page, tels que des factures, des reçus, etc.Le déploiement de votre application dans App Service comprend plusieurs étapes :
Préparer votre machine locale et votre instance d’application à l’aide des prérequis
Avant de créer votre App Service, préparez votre ordinateur local avec la configuration suivante :
Visual Studio 2019 et ses modules de développement d’applications pour Azure (consultez Fonctionnalités Azure dans Visual Studio ou utilisez Visual Studio Installer pour télécharger ces modules)
Les étapes préparatoires doivent être effectuées sur votre machine locale. En les réalisant, vous préparerez tous les paramètres et fichiers nécessaires pour commencer le déploiement de votre application :
Créez une archive contenant ABBYY FineReader Engine Library (par exemple, LibraryPackage.zip). La liste des fichiers figure dans le fichier FREngineDistribution.csv. Important ! Si vous disposez d’un espace de stockage limité (par exemple, si vous utilisez un App Service Plan avec 1 Go d’espace), nous vous recommandons d’utiliser l’option /extract pour créer un package ABBYY FineReader Engine personnalisé de taille minimale. Le reste de l’espace de stockage sera utilisé pour le traitement des fichiers. Lors de la création de l’archive, gardez à l’esprit que les paramètres de licence d’ABBYY FineReader Engine doivent être configurés en fonction des paramètres de la machine virtuelle :
Le fichier de jeton de licence en ligne doit se trouver dans le dossier Bin64.
Créez un compte de stockage Azure (frestorage dans cet article). Vous trouverez toutes les instructions nécessaires sur le site web Azure.
Créez votre App Service selon vos besoins (voir les instructions ici).
Créez deux conteneurs Blob dans frestorage :
fre-lib - pour les fichiers ABBYY FineReader Engine
processing-container - pour les résultats du traitement
Téléversez LibraryPackage.zip dans le conteneur fre-lib de la manière la plus pratique pour vous (à l’aide de .NET, PowerShell, d’un script Python ou des applications Azure Storage Explorer/Azure Portal).
Déployez et configurez la machine virtuelle avec les paramètres de licence dans votre compte Azure :
Installez l’utilitaire License Manager à l’aide de installLM.exe depuis LibraryPackage.zip.
Configurez le protocole réseau Sockets dans le fichier LicensingSettings.xml, puis redémarrez le service de licence.
Assurez-vous qu’Azure App Service peut accéder au port de connexion du service de licence (ajustez les règles du Pare-feu Windows sur la machine virtuelle).
Activez votre licence (pour la protection logicielle uniquement ; la protection en ligne ne nécessite pas d’activation).
Créez deux files d’attente dans frestorage :
processing-queue - pour définir les tâches de traitement des fichiers
status-queue - pour signaler l’achèvement des tâches
Créez deux projets Azure WebJob (.NET Framework) dans Visual Studio 2019 pour travailler avec frestorage :
FreDeployerJob - pour déployer LibraryPackage.zip vers App Service (voir la liste de ses fichiers : Config.cs, Functions.cs, Program.cs ci-dessous)
FreProcessorJob - pour le traitement des documents (voir la liste de ses fichiers : Config.cs, Functions.cs, Program.cs, EngineLoader.cs, IFileWriter.cs, Processor.cs ci-dessous)
Déploiement et exécution d’ABBYY FineReader Engine dans App Service
Pour déployer ABBYY FineReader Engine :
Publiez FreDeployerJob sur Azure App Service à l’aide de Visual Studio (définissez Triggered comme WebJob Type).
Ouvrez votre App Service dans le portail Azure.
Ouvrez la section WebJobs de votre App Service.
Recherchez FreDeployerJob dans la liste des WebJobs.
Lancez FreDeployerJob par un clic droit, puis la commande Run dans l’onglet WebJobs.
Vous pouvez accéder à l’onglet Logs pour vérifier le résultat du déploiement. Si celui-ci réussit, LibraryPackage.zip est téléversé depuis le conteneur fre-lib et déployé dans le dossier %HOME_EXPANDED% accessible à toutes les entités dans App Service.Pour déployer FreProcessorJob, publiez FreProcessorJob sur Azure App Service à l’aide de Visual Studio (définissez Continuous comme WebJob Type). FreProcessorJob apparaîtra alors dans la liste des onglets WebJobs de votre App Service.Pour traiter un fichier :
Téléversez le fichier que vous souhaitez traiter dans le processing-container.
Ajoutez un message JSON pour une nouvelle tâche de traitement au format {“blob-item-name” : “file_name”} dans le processing-queue. Si vous téléversez Demo.tif dans le processing-container, votre message doit être :
{"blob-item-name" : "Demo.tif"}
Attendez que la tâche soit terminée. Dès que la nouvelle tâche est définie, FreProcessorJob commence à traiter en mémoire le fichier spécifié. La status-queue contiendra des entrées concernant l’exécution de cette tâche.
Recherchez le fichier de sortie dans le processing-container.
FreProcessorJob fonctionne comme un process mono-thread. Si vous souhaitez traiter vos fichiers en parallèle, vous devez créer plusieurs FreProcessorJob qui écouteront la même file d’attente. 2. Chaque FreProcessorJob supplémentaire consomme davantage de mémoire. Tenez-en compte lors de l’achat de votre Service Plan. Par exemple, avec Azure Free Service Plan, il est préférable de n’avoir qu’un seul FreProcessorJob, qui consomme peu de mémoire et garantit ainsi la stabilité du traitement des fichiers. 3. L’utilisation d’un seul FreProcessorJob ne convient pas au traitement de documents volumineux comportant plusieurs pages. Dans ce cas, envisagez plutôt la reconnaissance de votre document dans Azure Cloud Service ou Azure Virtual Machine que dans App Service.
using System.IO;class Config{ // Chaîne de connexion au conteneur Blob public static readonly string ConnectionString = "your_connection_string"; // Le répertoire HOME_EXPANDED est commun à tous les dossiers WebJobs public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE"); // Nom des conteneurs d’entrée et de sortie dans votre compte de stockage public static readonly string LibraryContainerName = "fre-lib";
Functions.cs
namespace FreDeployerJob{ public class Functions { // Cette fonction ne sera pas déclenchée automatiquement ; vous devez la lancer manuellement [NoAutomaticTrigger] [Timeout("01:00:00")] public static void DeployFRE() { Console.WriteLine("Déploiement de FRE"); // Connexion au conteneur d’entrée existant <InputContainerName> à l’aide de la chaîne de connexion du compte de stockage BlobContainerClient inputContainerClient = new BlobContainerClient(Config.ConnectionString, Config.LibraryContainerName); // Création du répertoire de bibliothèque ainsi que des dossiers AppData et Temp pour l’initialisation d’ABBYY FineReader Engine 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")); // Parcours des blobs dans le conteneur. Le blob dans <InputContainerName> correspond à un fichier image foreach (BlobItem blobItem in inputContainerClient.GetBlobs()) { Console.WriteLine("\t" + blobItem.Name); // Recherche de la version allégée de la bibliothèque ABBYY FineReader Engine if (blobItem.Name == "LibraryPackage.zip") { Console.WriteLine("LibraryPackage.zip a été trouvé."); // Connexion au blob pour accéder à son contenu BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.LibraryContainerName, blobItem.Name); Console.WriteLine("Téléchargement en mémoire..."); // Télécharger le fichier zip en mémoire using (MemoryStream memoryStream = new MemoryStream()) { blobClient.DownloadTo(memoryStream); Console.WriteLine("LibraryPackage.zip a été téléchargé."); Console.WriteLine("Décompression..."); // Décompression sans utiliser de dossier temporaire (traitement uniquement en mémoire) 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 a été décompressé dans HOME_EXPANDED."); } } } } }}
Program.cs
namespace FreDeployerJob{ // Pour en savoir plus sur le SDK Microsoft Azure WebJobs, consultez https://go.microsoft.com/fwlink/?LinkID=320976 class Program { // Veuillez définir les chaînes de connexion suivantes dans app.config pour que ces WebJobs puissent s’exécuter : // AzureWebJobsDashboard et AzureWebJobsStorage static void Main() { // Une fois déclenché, ce WebJob appellera uniquement cette méthode Functions.DeployFRE(); } }}
using System;using System.IO;namespace FreProcessorJob{ class Config { // Chaîne de connexion au conteneur Blob public static readonly string ConnectionString = "your_connection_string"; // Le répertoire HOME_EXPANDED est commun à tous les dossiers WebJobs // Il est identique à celui du projet FreDeployerJob public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE"); // Nom du conteneur de traitement dans votre stockage public static readonly string ProcessingContainerName = "processing-container"; // Nom de la file d’attente de traitement public static readonly string ProcessingQueueName = "processing-queue"; public static readonly string StatusQueueName = "status-queue"; // Retourne le Customer Project ID pour ABBYY FineReader Engine public static String GetCustomerProjectId() { return "your_cpid"; } // Retourne le nom du jeton de licence en ligne // Si vous n’utilisez pas de licence en ligne, laissez une chaîne vide // Le jeton doit se trouver dans le dossier Bin64 du package de bibliothèque public static String GetLicenseTokenName() { return "your_online_license_token_if_you_have_it"; } // Retourne le mot de passe de la licence en ligne // Si vous n’utilisez pas de licence en ligne, laissez une chaîne vide public static String GetLicensePassword() { return "online_license_password_if_you have_it"; } // Retourne le nom du jeton de licence // La licence se trouve dans le dossier Bin64 du package de bibliothèque public static String GetLicenseTokenName() { return "your_licence_for_ABBYY FineReader Engine"; } // Retourne le mot de passe de la licence public static String GetLicensePassword() { return "license_password"; } // Retourne le chemin d’accès de l’Engine 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 { // Cette fonction est déclenchée/exécutée lorsqu'un nouveau message est écrit // dans une file d’attente Azure nommée processing-queue // Le message attendu est un message JSON avec la clé 'blob-item-name' // les résultats du traitement seront enregistrés dans le conteneur de traitement // l'état du traitement sera envoyé à status-queue au format JSON public static void ProcessQueueMessage([QueueTrigger("processing-queue")] string message) { // Tout d'abord, connexion à status-queue QueueClient queueClient = new QueueClient(Config.ConnectionString, Config.StatusQueueName); try { // Ceci sera consigné dans les journaux WebJob du portail Azure Console.WriteLine("Accepted task: " + message); JObject task = JObject.Parse(message); task["processor_id"] = Environment.GetEnvironmentVariable("WEBJOBS_NAME"); // Ceci sera envoyé à status-queue task["status"] = "accepted"; queueClient.SendMessage(task.ToString()); // Récupération de blob-item-name : le nom du fichier dans le conteneur de traitement string blobFileName = task["blob-item-name"].ToString(); BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, blobFileName); // Chargement du blob en mémoire Console.WriteLine("\t Downloading blob to memory: " + blobFileName); MemoryStream memoryStream = new MemoryStream(); blobClient.DownloadTo(memoryStream); Console.WriteLine("\t Downloaded."); // Mise à jour de l'état à processing Console.WriteLine("\t Processing in FRE: " + blobFileName); task["status"] = "processing"; queueClient.SendMessage(task.ToString()); // Traitement du blob téléchargé dans ABBYY FineReader Engine à l'aide des méthodes de traitement en mémoire // La sortie correspond au nom du résultat du traitement enregistré en tant que blob dans <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); } // Suppression de l'image d'entrée Console.WriteLine("\t Deleting from input container: " + blobFileName); blobClient.Delete(); // Mise à jour de l'état à succeeded // dans les journaux du portail Azure Console.WriteLine("Succeeded"); // dans status-queue task["status"] = "succeeded"; task["result-blob-name"] = resultBlobName; queueClient.SendMessage(task.ToString()); } catch (Exception error) { // En cas d'erreur, signalement // dans les journaux du portail Azure Console.WriteLine("Failed: " + error.Message); // dans 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{ // Pour en savoir plus sur le SDK Microsoft Azure WebJobs, consultez https://go.microsoft.com/fwlink/?LinkID=320976 class Program { // Veuillez définir les chaînes de connexion suivantes dans app.config pour que ces WebJobs puissent s'exécuter : // AzureWebJobsDashboard and AzureWebJobsStorage static void Main() { var config = new JobHostConfiguration(); if (config.IsDevelopment) { config.UseDevelopmentSettings(); } // ABBYY FineReader Engine n'est pas thread-safe ; nous ne pouvons donc pas traiter plus d'un message à la fois config.Queues.BatchSize = 1; var host = new JobHost(config); // Le code suivant garantit que le WebJob s'exécutera en continu // car l'une des fonctions est liée à une file d’attente Azure et attend les nouvelles tâches host.RunAndBlock(); } }}
EngineLoader.cs
using System;using System.IO;using System.Runtime.InteropServices;using FREngine;namespace FreProcessorJob.FreProcessor{ // Classe pour le chargement/déchargement de FREngine.dll et l'initialisation/désinitialisation du Engine // Le chargement est effectué dans le constructeur, le déchargement dans Dispose() // Lève des exceptions en cas d'échec du chargement public class EngineLoader : IDisposable { // Charger ABBYY FineReader Engine avec les paramètres stockés dans 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 { // Charger la bibliothèque FREngine.dll dllHandle = LoadLibraryEx(enginePath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH); if (dllHandle == IntPtr.Zero) { int error = Marshal.GetLastWin32Error(); Console.WriteLine("La dernière erreur Win32 était : " + error); throw new Exception("Impossible de charger " + enginePath); } IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine"); if (initializeEnginePtr == IntPtr.Zero) { throw new Exception("Impossible de trouver la fonction InitializeEngine"); } IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine"); if (deinitializeEnginePtr == IntPtr.Zero) { throw new Exception("Impossible de trouver la fonction DeinitializeEngine"); } IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow"); if (dllCanUnloadNowPtr == IntPtr.Zero) { throw new Exception("Impossible de trouver la fonction DllCanUnloadNow"); } // Convertir les pointeurs en délégués initializeEngine = (InitializeEngine)Marshal.GetDelegateForFunctionPointer( initializeEnginePtr, typeof(InitializeEngine)); deinitializeEngine = (DeinitializeEngine)Marshal.GetDelegateForFunctionPointer( deinitializeEnginePtr, typeof(DeinitializeEngine)); dllCanUnloadNow = (DllCanUnloadNow)Marshal.GetDelegateForFunctionPointer( dllCanUnloadNowPtr, typeof(DllCanUnloadNow)); // Appeler la fonction InitializeEngine 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) { // Libérer la bibliothèque FREngine.dll engine = null; // Suppression de tous les objets avant l'appel à FreeLibrary GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); FreeLibrary(dllHandle); dllHandle = IntPtr.Zero; initializeEngine = null; deinitializeEngine = null; dllCanUnloadNow = null; throw; } } // Décharger ABBYY FineReader Engine public void Dispose() { if (engine == null) { // Le Engine n'a pas été chargé return; } engine = null; int hresult = deinitializeEngine(); // Suppression de tous les objets avant l'appel à FreeLibrary GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); hresult = dllCanUnloadNow(); if (hresult == 0) { FreeLibrary(dllHandle); } dllHandle = IntPtr.Zero; initializeEngine = null; deinitializeEngine = null; dllCanUnloadNow = null; // Levée d'exception après le nettoyage Marshal.ThrowExceptionForHR(hresult); } // Retourne un pointeur vers l'objet principal de ABBYY FineReader Engine public IEngine Engine { get { return engine; } } // Fonctions de Kernel32.dll [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); // Fonctions de FREngine.dll [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(); // Variables privées private FREngine.IEngine engine = null; // Handle vers 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() { // Création d'une connexion à un nouveau blob dans <ProcessingContainerName>. Le résultat du traitement y sera stocké BlobClient resultBlobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, resultBlobName + fileExtension); // Écraser le fichier existant resultBlobClient.DeleteIfExists(); // Définir la position sur 0 pour écrire le fichier depuis le début stream.Position = 0; resultBlobClient.Upload(stream); stream.Close(); } public void Dispose() { // Fermer le flux mémoire lors de la libération afin de pouvoir y accéder une fois les données écrites 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("Chargement du profil prédéfini..."); // ceci est facultatif engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy"); // ceci est obligatoire sur les App Service Plan peu performants, car des erreurs se produiront lors du traitement parallèle engineLoader.Engine.MultiProcessingParams.MultiProcessingMode = MultiProcessingModeEnum.MPM_Sequential; } private void LoadEngine() { try { if (engineLoader == null) { engineLoader = new EngineLoader(); } setupFREngine(); } catch (Exception error) { displayMessage("erreur : " + error.Message); } } private void UnloadEngine() { try { if (engineLoader != null) { engineLoader.Dispose(); engineLoader = null; } } catch (Exception error) { displayMessage("erreur : " + 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; // Ajouter le fichier image au document displayMessage("Chargement de l'image..."); IntPtr handle = Marshal.AllocHGlobal(inputMemoryStream.GetBuffer().Length); Marshal.Copy(inputMemoryStream.GetBuffer(), 0, handle, inputMemoryStream.GetBuffer().Length); document.AddImageFileFromMemory(handle.ToInt64(), null, null); // Reconnaître le document displayMessage("Reconnaissance..."); document.Process(null); // Enregistrer les résultats displayMessage("Enregistrement des résultats..."); FileWriter fileWriter = new FileWriter(inputBlobName, ".pdf"); resultBlobName = inputBlobName + ".pdf"; document.ExportToMemory(fileWriter, FREngine.FileExportFormatEnum.FEF_PDF, null); } catch (Exception error) { displayMessage("erreur : " + error.Message); throw error; } finally { // Fermer le document document.Close(); } return resultBlobName; } public void Dispose() { UnloadEngine(); } }}