Use this file to discover all available pages before exploring further.
Esta sección ofrece instrucciones para implementar una aplicación FRE 12 for Windows en Azure App Service. A modo de ejemplo, se presenta un par de proyectos de WebJob que usan datos de una cuenta de Azure Storage. El procesamiento de archivos se realiza mediante un contenedor Blob. Con este escenario, obtendrá los mejores resultados de reconocimiento para documentos pequeños de una sola página, como facturas, recibos, etc.La implementación de su aplicación en App Service incluye varios pasos:
Configurar el equipo local y la instancia de la aplicación según los requisitos previos
Antes de crear su App Service, utilice la siguiente especificación para preparar su máquina local:
Visual Studio 2019 y sus módulos para desarrollar aplicaciones para Azure (consulte Azure feature in Visual Studio o use Visual Studio Installer para descargar dichos módulos)
Envoltorio de ABBYY FineReader Engine para .NET Framework 4.7 (en la carpeta C:\ProgramData\ABBYY\SDK\12\FineReader Engine\Inc.NET interops después de la instalación para desarrolladores)
Interfaz IFileWriter sobrescrita para trabajar con los contenedores Blob (consulte el ejemplo a continuación)
Azure Storage Explorer (opcional - descargue aquí)
Máquina virtual en su cuenta de Azure destinada a los procedimientos de licenciamiento. Para la configuración adicional, necesita:
Dirección IP
Puerto de conexión abierto (predeterminado o definido por el usuario). Para abrirlo, use un firewall
Los pasos preparatorios deben realizarse en su máquina local. Al completar estos pasos, preparará todas las configuraciones y archivos necesarios para comenzar a desplegar su aplicación:
Cree un paquete comprimido con la biblioteca ABBYY FineReader Engine Library (por ejemplo, LibraryPackage.zip). La lista de archivos se encuentra en el archivo FREngineDistribution.csv. ¡Importante! Si tiene espacio de almacenamiento limitado (por ejemplo, usa un App Service Plan con 1 GB de espacio), recomendamos usar la opción /extract para crear su paquete personalizado de ABBYY FineReader Engine de tamaño mínimo. El resto del espacio de almacenamiento se usará para procesar los archivos. Al crear el paquete, tenga en cuenta que las configuraciones de licencias de ABBYY FineReader Engine deben establecerse de acuerdo con las configuraciones de la máquina virtual:
El archivo de token de licencia en línea debe ubicarse en la carpeta Bin64.
Cree una cuenta de Azure Storage (frestorage en este artículo). Puede encontrar todas las instrucciones necesarias en el sitio web de Azure.
Cree su App Service como desee (consulte las instrucciones aquí).
Cree dos contenedores Blob dentro de frestorage:
fre-lib - para los archivos de ABBYY FineReader Engine
processing-container - para los resultados del procesamiento
Cargue LibraryPackage.zip en el contenedor fre-lib de la manera más conveniente (usando .NET, PowerShell, un script de Python o las aplicaciones Azure Storage Explorer/Azure Portal).
Despliegue y configure la máquina virtual con las configuraciones de licencias en su cuenta de Azure:
Instale la utilidad License Manager mediante installLM.exe desde LibraryPackage.zip.
Configure el protocolo de red Sockets en el archivo LicensingSettings.xml y, a continuación, reinicie el servicio de licencias.
Asegúrese de que Azure App Service pueda acceder al puerto de conexión del servicio de licencias (ajuste las reglas del Firewall de Windows en la máquina virtual).
Active su licencia (solo para protección de software; la protección en línea no requiere activación).
Cree dos colas dentro de frestorage:
processing-queue - para las tareas de procesamiento de archivos
status-queue - para notificar la finalización de las tareas
Cree dos proyectos Azure WebJob (.NET Framework) en Visual Studio 2019 para trabajar con frestorage:
FreDeployerJob - para desplegar LibraryPackage.zip en App Service (consulte la lista de sus archivos: Config.cs, Functions.cs, Program.cs a continuación)
FreProcessorJob - para el procesamiento de documentos (consulte la lista de sus archivos: Config.cs, Functions.cs, Program.cs, EngineLoader.cs, IFileWriter.cs, Processor.cs a continuación)
Implementar y ejecutar ABBYY FineReader Engine en App Service
Para implementar ABBYY FineReader Engine:
Publique FreDeployerJob en Azure App Service con Visual Studio (establezca Triggered como WebJob Type).
Abra su App Service en el portal de Azure.
Abra la sección WebJobs de su App Service.
Busque FreDeployerJob en la lista de WebJobs.
Inicie FreDeployerJob con el comando Run al hacer clic con el botón derecho en la pestaña WebJobs.
Puede acceder a la pestaña Logs para comprobar el resultado de la implementación. Si se realiza correctamente, LibraryPackage.zip se carga desde el contenedor fre-lib y se implementa en la carpeta %HOME_EXPANDED%, disponible para todas las entidades de App Service.Para implementar FreProcessorJob, publique FreProcessorJob en Azure App Service con Visual Studio (establezca Continuous como WebJob Type). Como resultado, FreProcessorJob aparecerá en la lista de WebJobs de su App Service.Para procesar un archivo:
Cargue el archivo que desea procesar en processing-container.
Agregue a processing-queue un mensaje JSON para una nueva tarea de procesamiento con el formato {“blob-item-name” : “file_name”}. Si carga Demo.tif en processing-container, el mensaje debe ser:
{"blob-item-name" : "Demo.tif"}
Espere a que la tarea se complete. En cuanto se establece la nueva tarea, FreProcessorJob empieza a procesar el archivo especificado en memoria. La status-queue contendrá entradas sobre la ejecución de esta tarea.
Encuentre el archivo de salida en processing-container.
FreProcessorJob funciona como un proceso de un solo hilo. Si tiene previsto procesar sus archivos en paralelo, debe crear varios FreProcessorJob que escuchen la misma cola. 2. Cada FreProcessorJob adicional consume memoria extra. Tenga esto en cuenta al adquirir su Service Plan. Por ejemplo, en Azure Free Service Plan, se recomienda tener solo un FreProcessorJob que consuma poca memoria y, de este modo, garantice la estabilidad del procesamiento de archivos. 3. Usar un solo FreProcessorJob no es adecuado para procesar documentos grandes de varias páginas. En este caso, considere el reconocimiento del documento en Azure Cloud Service o Azure Virtual Machine en lugar de App Service.
using System.IO;class Config{ // Cadena de conexión al contenedor de blobs public static readonly string ConnectionString = "your_connection_string"; // El directorio HOME_EXPANDED es común para todas las carpetas de WebJobs public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE"); // Nombres de los contenedores de entrada y salida en su almacenamiento public static readonly string LibraryContainerName = "fre-lib";
Functions.cs
namespace FreDeployerJob{ public class Functions { // Esta función no se activará automáticamente; debe hacerlo manualmente [NoAutomaticTrigger] [Timeout("01:00:00")] public static void DeployFRE() { Console.WriteLine("Deploying FRE"); // Conexión al contenedor de entrada existente <InputContainerName> mediante la cadena de conexión de la cuenta de almacenamiento BlobContainerClient inputContainerClient = new BlobContainerClient(Config.ConnectionString, Config.LibraryContainerName); // Creación del directorio de la biblioteca, así como de las carpetas AppData y Temp para inicializar 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")); // Recorrido de los blobs del contenedor. El blob en <InputContainerName> equivale a algún archivo de imagen foreach (BlobItem blobItem in inputContainerClient.GetBlobs()) { Console.WriteLine("\t" + blobItem.Name); // Búsqueda de la versión ligera de la biblioteca ABBYY FineReader Engine if (blobItem.Name == "LibraryPackage.zip") { Console.WriteLine("LibraryPackage.zip was found."); // Conexión al blob para acceder a su contenido BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.LibraryContainerName, blobItem.Name); Console.WriteLine("Downloading to memory..."); // Descargar el archivo zip en memoria using (MemoryStream memoryStream = new MemoryStream()) { blobClient.DownloadTo(memoryStream); Console.WriteLine("LibraryPackage.zip was downloaded."); Console.WriteLine("Unzipping..."); // Descomprimir sin usar la carpeta temporal (solo procesamiento en memoria) 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{ // Para obtener más información sobre Microsoft Azure WebJobs SDK, consulte https://go.microsoft.com/fwlink/?LinkID=320976 class Program { // Configure las siguientes cadenas de conexión en app.config para que estos WebJobs se ejecuten: // AzureWebJobsDashboard and AzureWebJobsStorage static void Main() { // Cuando se active, este WebJob solo llamará a este método Functions.DeployFRE(); } }}
using System;using System.IO;namespace FreProcessorJob{ class Config { // Cadena de conexión al contenedor de blobs public static readonly string ConnectionString = "your_connection_string"; // El directorio HOME_EXPANDED es común para todas las carpetas de WebJobs // Es el mismo que en el proyecto FreDeployerJob public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE"); // Nombre del contenedor de procesamiento en su almacenamiento public static readonly string ProcessingContainerName = "processing-container"; // Nombre de la cola de procesamiento public static readonly string ProcessingQueueName = "processing-queue"; public static readonly string StatusQueueName = "status-queue"; // Devuelve el Customer Project ID de ABBYY FineReader Engine public static String GetCustomerProjectId() { return "your_cpid"; } // Devuelve el nombre del token de licencia en línea // Si no usa una licencia en línea, deje una cadena vacía // El token debe estar ubicado en la carpeta Bin64 del paquete de la biblioteca public static String GetLicenseTokenName() { return "your_online_license_token_if_you_have_it"; } // Devuelve la contraseña de la licencia en línea // Si no usa una licencia en línea, deje una cadena vacía public static String GetLicensePassword() { return "online_license_password_if_you have_it"; } // Devuelve el nombre del token de licencia // La gestión de licencias se encuentra en la carpeta Bin64 del paquete de la biblioteca public static String GetLicenseTokenName() { return "your_licence_for_ABBYY FineReader Engine"; } // Devuelve la contraseña de la licencia public static String GetLicensePassword() { return "license_password"; } // Devuelve la ruta del motor 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 { // Esta función se activa o ejecuta cuando se escribe un mensaje nuevo // en una cola de Azure llamada processing-queue // Se espera que el mensaje sea un mensaje JSON con la clave 'blob-item-name' // los resultados del procesamiento se guardarán en el contenedor processing // el estado del procesamiento se enviará a status-queue en formato JSON public static void ProcessQueueMessage([QueueTrigger("processing-queue")] string message) { // Primero, se conecta a status-queue QueueClient queueClient = new QueueClient(Config.ConnectionString, Config.StatusQueueName); try { // Esto se registrará en los logs de WebJob en el portal de Azure Console.WriteLine("Accepted task: " + message); JObject task = JObject.Parse(message); task["processor_id"] = Environment.GetEnvironmentVariable("WEBJOBS_NAME"); // Esto se enviará a status-queue task["status"] = "accepted"; queueClient.SendMessage(task.ToString()); // Obtención de blob-item-name: el nombre del archivo en el contenedor Processing string blobFileName = task["blob-item-name"].ToString(); BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, blobFileName); // Carga del blob en memoria Console.WriteLine("\t Downloading blob to memory: " + blobFileName); MemoryStream memoryStream = new MemoryStream(); blobClient.DownloadTo(memoryStream); Console.WriteLine("\t Downloaded."); // Actualización del estado a processing Console.WriteLine("\t Processing in FRE: " + blobFileName); task["status"] = "processing"; queueClient.SendMessage(task.ToString()); // Procesamiento del blob descargado en ABBYY FineReader Engine mediante métodos de procesamiento en memoria // La salida es el nombre del resultado del procesamiento guardado como blob en <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); } // Eliminación de la imagen de entrada Console.WriteLine("\t Deleting from input container: " + blobFileName); blobClient.Delete(); // Actualización del estado a succeeded // en los logs del portal de Azure Console.WriteLine("Succeeded"); // en status-queue task["status"] = "succeeded"; task["result-blob-name"] = resultBlobName; queueClient.SendMessage(task.ToString()); } catch (Exception error) { // En caso de error, se informa // en los logs del portal de Azure Console.WriteLine("Failed: " + error.Message); // a 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{ // Para obtener más información sobre Microsoft Azure WebJobs SDK, consulte https://go.microsoft.com/fwlink/?LinkID=320976 class Program { // Configure las siguientes cadenas de conexión en app.config para que estos WebJobs se ejecuten: // AzureWebJobsDashboard y AzureWebJobsStorage static void Main() { var config = new JobHostConfiguration(); if (config.IsDevelopment) { config.UseDevelopmentSettings(); } // ABBYY FineReader Engine no es seguro para subprocesos, por lo que no podemos procesar más de un mensaje a la vez config.Queues.BatchSize = 1; var host = new JobHost(config); // El siguiente código garantiza que el WebJob se ejecute continuamente // ya que una de las funciones está vinculada a una cola de Azure y escucha nuevas tareas host.RunAndBlock(); } }}
EngineLoader.cs
using System;using System.IO;using System.Runtime.InteropServices;using FREngine;namespace FreProcessorJob.FreProcessor{ // Clase para cargar/descargar FREngine.dll e inicializar/desinicializar el motor // La carga se realiza en el constructor y la descarga en Dispose() // Lanza excepciones cuando la carga falla public class EngineLoader : IDisposable { // Cargar ABBYY FineReader Engine con la configuración almacenada en 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 { // Cargar la biblioteca FREngine.dll dllHandle = LoadLibraryEx(enginePath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH); if (dllHandle == IntPtr.Zero) { int error = Marshal.GetLastWin32Error(); Console.WriteLine("El último error de Win32 fue: " + error); throw new Exception("No se puede cargar " + enginePath); } IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine"); if (initializeEnginePtr == IntPtr.Zero) { throw new Exception("No se encuentra la función InitializeEngine"); } IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine"); if (deinitializeEnginePtr == IntPtr.Zero) { throw new Exception("No se encuentra la función DeinitializeEngine"); } IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow"); if (dllCanUnloadNowPtr == IntPtr.Zero) { throw new Exception("No se encuentra la función DllCanUnloadNow"); } // Convertir punteros a delegados initializeEngine = (InitializeEngine)Marshal.GetDelegateForFunctionPointer( initializeEnginePtr, typeof(InitializeEngine)); deinitializeEngine = (DeinitializeEngine)Marshal.GetDelegateForFunctionPointer( deinitializeEnginePtr, typeof(DeinitializeEngine)); dllCanUnloadNow = (DllCanUnloadNow)Marshal.GetDelegateForFunctionPointer( dllCanUnloadNowPtr, typeof(DllCanUnloadNow)); // Llamar a la función 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) { // Liberar la biblioteca FREngine.dll engine = null; // Eliminar todos los objetos antes de llamar a FreeLibrary GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); FreeLibrary(dllHandle); dllHandle = IntPtr.Zero; initializeEngine = null; deinitializeEngine = null; dllCanUnloadNow = null; throw; } } // Descargar ABBYY FineReader Engine public void Dispose() { if (engine == null) { // El motor no fue cargado return; } engine = null; int hresult = deinitializeEngine(); // Eliminar todos los objetos antes de llamar a FreeLibrary GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); hresult = dllCanUnloadNow(); if (hresult == 0) { FreeLibrary(dllHandle); } dllHandle = IntPtr.Zero; initializeEngine = null; deinitializeEngine = null; dllCanUnloadNow = null; // Lanzar excepción tras la limpieza Marshal.ThrowExceptionForHR(hresult); } // Devuelve un puntero al objeto principal de ABBYY FineReader Engine public IEngine Engine { get { return engine; } } // Funciones 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); // Funciones 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 privadas private FREngine.IEngine engine = null; // Handle de 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() { // Crear una conexión a un nuevo blob en <ProcessingContainerName>. El resultado del procesamiento se almacenará allí BlobClient resultBlobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, resultBlobName + fileExtension); // Sobrescribir el archivo existente resultBlobClient.DeleteIfExists(); // Establecer la posición en 0 para escribir el archivo desde el principio stream.Position = 0; resultBlobClient.Upload(stream); stream.Close(); } public void Dispose() { // Cerrar el flujo de memoria al liberar el objeto para poder acceder a él después de escribir los datos 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("Cargando perfil predefinido..."); // esto es opcional engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy"); // esto es obligatorio en planes de App Service de bajo rendimiento, ya que se producirán errores en el procesamiento paralelo 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; // Agregar un archivo de imagen al documento displayMessage("Cargando imagen..."); IntPtr handle = Marshal.AllocHGlobal(inputMemoryStream.GetBuffer().Length); Marshal.Copy(inputMemoryStream.GetBuffer(), 0, handle, inputMemoryStream.GetBuffer().Length); document.AddImageFileFromMemory(handle.ToInt64(), null, null); // Reconocer el documento displayMessage("Reconociendo..."); document.Process(null); // Guardar los resultados displayMessage("Guardando resultados..."); 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 { // Cerrar el documento document.Close(); } return resultBlobName; } public void Dispose() { UnloadEngine(); } }}