Saltar al contenido principal
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:
  1. Configurar el equipo local y la instancia de la aplicación según los requisitos previos
  2. Realizar los pasos preparatorios antes de implementar su aplicación
  3. Implementar y ejecutar su aplicación en App Service
Use los ejemplos de código que se muestran en las instrucciones siguientes.

Requisitos previos

Máquina local

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)
  • Azure SDK (descargue aquí)
  • .NET Framework 4.7.2
  • Paquete NuGet para trabajar con Azure Storage y contenedores Blob:
    • Azure.Storage.Blobs (descargue aquí)
    • Azure.Storage.Queues (descargue aquí)
    • Newtonsoft.Json (descargue aquí)
    • System.IO.Compression.ZipFile (descargue aquí)
  • 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
    • Detalles de conexión del protocolo de red Sockets

Pasos preparatorios

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:
  1. 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 LicensingSettings.xml debe configurarse para la configuración de red (consulte Trabajo con el archivo LicensingSettings.xml).
    • Debe usarse el protocolo de red Sockets.
    • El archivo de token de licencia en línea debe ubicarse en la carpeta Bin64.
  2. Cree una cuenta de Azure Storage (frestorage en este artículo). Puede encontrar todas las instrucciones necesarias en el sitio web de Azure.
  3. Cree su App Service como desee (consulte las instrucciones aquí).
  4. Cree dos contenedores Blob dentro de frestorage:
    • fre-lib - para los archivos de ABBYY FineReader Engine
    • processing-container - para los resultados del procesamiento
  5. 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).
  6. 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).
  7. 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
  8. 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:
  1. Publique FreDeployerJob en Azure App Service con Visual Studio (establezca Triggered como WebJob Type).
  2. Abra su App Service en el portal de Azure.
  3. Abra la sección WebJobs de su App Service.
  4. Busque FreDeployerJob en la lista de WebJobs.
  5. 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:
  1. Cargue el archivo que desea procesar en processing-container.
  2. 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"}
  1. 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.
  2. Encuentre el archivo de salida en processing-container.
  1. 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.

Ejemplos de código

Esta sección incluye ejemplos de código para desplegar e implementar la API de ABBYY FineReader Engine en App Service.

FreDeployerJob:

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";
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.");
                    }
                }
            }
        }
    }
}
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();
        }
    }
}

FreProcessorJob:

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;
        }
    }
}
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());
            }
        }
    }
}
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();
        }
    }
}
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;
    }
}
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;
    }
}
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();
        }
    }
}