跳转到主要内容
本节说明如何将 FRE 12 for Windows 应用程序部署到 Azure App Service。这里以一对使用 Azure 存储帐户中数据的 WebJob 项目为例。文件处理通过 Blob 容器完成。
在此方案中,对于发票、收据等小型单页文档,您可以获得最佳识别结果。
将应用程序部署到 App Service 包括以下几个步骤:
  1. 根据前提条件配置本地计算机和应用实例
  2. 在部署应用程序之前执行准备步骤
  3. 在 App Service 中部署并运行应用程序
请使用以下说明中提供的代码示例

前提条件

本地计算机

在创建 App Service 之前,请根据以下要求准备本地计算机环境:
  • Visual Studio 2019 及其用于开发 Azure 应用程序的模块 (请参阅 Visual Studio 中的 Azure 功能,或使用 Visual Studio Installer 下载此类模块)
  • Azure SDK (在此处下载)
  • .NET Framework 4.7.2
  • 用于 Azure Storage 和 Blob 容器的 NuGet 包:
    • Azure.Storage.Blobs (在此处下载)
    • Azure.Storage.Queues (在此处下载)
    • Newtonsoft.Json (在此处下载)
    • System.IO.Compression.ZipFile (在此处下载)
  • 适用于 .Net Framework 4.7 的 ABBYY FineReader Engine 封装器 (开发人员安装完成后,位于 C:\\ProgramData\\ABBYY\\SDK\\12\\FineReader Engine\\Inc\\.NET interops 文件夹中)
  • 为 Blob 容器操作而重写的 IFileWriter interface (请参见下方的示例)
  • Azure Storage Explorer (可选——在此处下载)
  • Azure 帐户中的虚拟机,用于执行许可流程。后续配置需要以下信息:
    • IP 地址
    • 开放的连接端口 (默认或用户定义) 。请使用防火墙将其开放
    • Sockets 网络协议的连接详细信息

准备步骤

以下准备步骤需在本地计算机上完成。完成这些步骤后,您就已准备好部署应用程序所需的全部设置和文件:
  1. 创建一个包含 ABBYY FineReader Engine Library 的压缩包 (例如 LibraryPackage.zip) 。文件列表见 FREngineDistribution.csv 文件。
    重要提示!如果您的存储空间有限 (例如,您使用的是具有 1 GB 空间的 App Service 计划) ,建议使用 /extract 选项创建自定义的最小体积 ABBYY FineReader Engine 包。其余存储空间将用于处理文件。
    创建压缩包时,请注意,ABBYY FineReader Engine 的许可设置必须根据虚拟机配置进行设置:
    • 必须将 LicensingSettings.xml 文件配置为 Network 模式 (请参见 Working with the LicensingSettings.xml File) 。
    • 必须使用 Sockets 网络协议。
    • Online License 令牌文件必须位于 Bin64 文件夹中。
  2. 创建一个 Azure 存储帐户 (本文中使用 frestorage) 。所需说明均可在 Azure 网站上找到。
  3. 按需创建您的 App Service (请参见此处的说明) 。
  4. frestorage 中创建两个 Blob 容器:
    • fre-lib - 用于存放 ABBYY FineReader Engine 文件
    • processing-container - 用于存放处理结果
  5. 以最方便的方式将 LibraryPackage.zip 上传到 fre-lib 容器 (可使用 .NET、Powershell、Python 脚本或 Azure Storage Explorer/Azure Portal 应用程序) 。
  6. 在您的 Azure 账户中部署并配置带有许可设置的虚拟机:
    • 通过 LibraryPackage.zip 中的 installLM.exe 安装 License Manager 实用程序。
    • LicensingSettings.xml 中设置 Sockets 网络协议,然后重启许可服务。
    • 确保 Azure App Service 可以访问许可服务的连接端口 (调整虚拟机上的 Windows 防火墙规则) 。
    • 激活您的许可证 (仅适用于软件保护;在线保护无需激活) 。
  7. frestorage 中创建两个队列:
    • processing-queue - 用于设置文件处理任务
    • status-queue - 用于通知任务完成
  8. 在 Visual Studio 2019 中创建两个 Azure WebJob (.NET Framework) 项目,以便使用 frestorage
    • FreDeployerJob - 用于将 LibraryPackage.zip 部署到 App Service (其文件列表见下文Config.csFunctions.csProgram.cs)
    • FreProcessorJob - 用于文档处理 (其文件列表见下文Config.csFunctions.csProgram.csEngineLoader.csIFileWriter.csProcessor.cs)

在 App Service 中部署和运行 ABBYY FineReader Engine

要部署 ABBYY FineReader Engine:
  1. 使用 Visual Studio 将 FreDeployerJob 发布到 Azure App Service (将 WebJob Type 设为 Triggered) 。
  2. 在 Azure 门户中打开 App Service。
  3. 打开该 App Service 的 WebJobs。
  4. 在 WebJobs 列表中找到 FreDeployerJob。
  5. 在 WebJobs 选项卡中右键单击 FreDeployerJob,然后选择 Run 命令启动它。
你可以打开 Logs 选项卡查看部署结果。如果部署成功,LibraryPackage.zip 将从 fre-lib 容器上传,并部署到 %HOME_EXPANDED% 文件夹中,App Service 中的所有实体都可以访问该文件夹。 要部署 FreProcessorJob,请使用 Visual Studio 将 FreProcessorJob 发布到 Azure App Service (将 WebJob Type 设为 Continuous) 。发布后,FreProcessorJob 将显示在 App Service 的 WebJobs 选项卡列表中。 要处理文件:
  1. 将要处理的文件上传到 processing-container。
  2. 按照 {“blob-item-name” : “file_name”} 格式向 processing-queue 添加一条新的处理任务 JSON 消息。如果你将 Demo.tif 上传到 processing-container,则消息应为:
{"blob-item-name" : "Demo.tif"}
  1. 等待任务完成。新任务一经创建,FreProcessorJob 就会开始在内存中处理指定文件。status-queue 中将包含有关此任务执行情况的条目。
  2. 在 processing-container 中查找输出文件。
  1. FreProcessorJob 以单线程进程方式运行。如果您打算并行处理文件,则需要创建多个 FreProcessorJob 来监听同一个队列。2. 每增加一个 FreProcessorJob 都会消耗额外的内存。购买 Service Plan 时请将这一点考虑在内。例如,在 Azure Free Service Plan 中,最好只使用一个占用较少内存的 FreProcessorJob,这样可以确保文件处理的稳定性。3. 单个 FreProcessorJob 不适合处理大型多页文档。在这种情况下,建议改为在 Azure Cloud Service 或 Azure Virtual Machine 中对文档进行识别,而不是使用 App Service。

代码示例

本节包含用于在 App Service 中部署和集成 ABBYY FineReader Engine API 的代码示例。

FreDeployerJob:

using System.IO;
class Config
{
    // blob 容器的连接字符串
    public static readonly string ConnectionString = "your_connection_string";
    // HOME_EXPANDED 目录是所有 WebJobs 文件夹共用的
    public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE");
    // 存储中的输入和输出容器名称
    public static readonly string LibraryContainerName = "fre-lib";
namespace FreDeployerJob
{
    public class Functions
    {
        // 此函数不会自动触发,需要手动执行
        [NoAutomaticTrigger]
        [Timeout("01:00:00")]
        public static void DeployFRE()
        {
            Console.WriteLine("Deploying FRE");
            // 通过存储帐户连接字符串连接到现有输入容器 <InputContainerName>
            BlobContainerClient inputContainerClient = new BlobContainerClient(Config.ConnectionString, Config.LibraryContainerName);
            // 创建库目录,以及 ABBYY FineReader Engine 初始化所需的 AppData 和 Temp 文件夹
            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"));
            // 遍历容器中的 blob。<InputContainerName> 中的每个 blob 都对应一个图像文件
            foreach (BlobItem blobItem in inputContainerClient.GetBlobs())
            {
                Console.WriteLine("\t" + blobItem.Name);
                // 查找轻量版 ABBYY FineReader Engine 库
                if (blobItem.Name == "LibraryPackage.zip")
                {
                    Console.WriteLine("LibraryPackage.zip was found.");
                    // 连接到 blob 以访问其内容
                    BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.LibraryContainerName, blobItem.Name);
                    Console.WriteLine("Downloading to memory...");
                    // 将 zip 下载到内存
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        blobClient.DownloadTo(memoryStream);
                        Console.WriteLine("LibraryPackage.zip was downloaded.");
                        Console.WriteLine("Unzipping...");
                        // 不使用临时文件夹进行解压(仅在内存中处理)
                        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
{
    // 要了解有关 Microsoft Azure WebJobs SDK 的详细信息,请参阅 https://go.microsoft.com/fwlink/?LinkID=320976
    class Program
    {
        // 请在 app.config 中为这些 WebJobs 设置以下连接字符串,以便其运行:
        // AzureWebJobsDashboard 和 AzureWebJobsStorage
        static void Main()
        {
            // 触发后,此 WebJob 只会调用此方法
            Functions.DeployFRE();
        }
    }
}

FreProcessorJob:

using System;
using System.IO;
namespace FreProcessorJob
{
    class Config
    {
        // blob container 的连接字符串
        public static readonly string ConnectionString = "your_connection_string";
        // HOME_EXPANDED 目录是所有 WebJobs 文件夹共用的
        // 与 FreDeployerJob 项目中的相同
        public static readonly string LibraryFolder = Path.Combine(System.Environment.GetEnvironmentVariable("HOME_EXPANDED"), "FRE");
        // 存储中的 processing-container 容器名称
        public static readonly string ProcessingContainerName = "processing-container";
        // 处理队列名称
        public static readonly string ProcessingQueueName = "processing-queue";
        public static readonly string StatusQueueName = "status-queue";
        // 返回 ABBYY FineReader Engine 的 Customer Project ID
        public static String GetCustomerProjectId()
        {
            return "your_cpid";
        }
 
        // 返回在线许可证令牌名称
        // 如果不使用在线许可证,请保留空字符串
        // 令牌应位于库包的 Bin64 文件夹中
        public static String GetLicenseTokenName()
        {
            return "your_online_license_token_if_you_have_it";
        }
 
        // 返回在线许可证密码
        // 如果不使用在线许可证,请保留空字符串
        public static String GetLicensePassword()
        {
            return "online_license_password_if_you have_it";
        }
 
        // 返回许可证令牌名称
        // 许可文件位于库包的 Bin64 文件夹中
        public static String GetLicenseTokenName()
        {
            return "your_licence_for_ABBYY FineReader Engine";
        }
 
        // 返回许可证密码
        public static String GetLicensePassword()
        {
            return "license_password";
        }
 
        // 返回引擎路径
        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
    {
        // 当有新消息写入
        // 名为 processing-queue 的 Azure 队列时,此函数会被触发/执行
        // 该消息应为包含“blob-item-name”键的 JSON 消息
        // 处理结果将保存到 processing 容器
        // 处理状态将以 JSON 格式发送到 status-queue
        public static void ProcessQueueMessage([QueueTrigger("processing-queue")] string message)
        {
            // 首先,连接到 status-queue
            QueueClient queueClient = new QueueClient(Config.ConnectionString, Config.StatusQueueName);
            try
            {
                // 这将记录到 Azure 门户中的 WebJob 日志
                Console.WriteLine("Accepted task: " + message);
                JObject task = JObject.Parse(message);
                task["processor_id"] = Environment.GetEnvironmentVariable("WEBJOBS_NAME");
 
                // 这将发送到 status-queue
                task["status"] = "accepted";
                queueClient.SendMessage(task.ToString());
 
                // 获取 blob-item-name,即 Processing 容器中的文件名
                string blobFileName = task["blob-item-name"].ToString();
                BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.ProcessingContainerName, blobFileName);
                // 将 blob 加载到内存中
                Console.WriteLine("\t Downloading blob to memory: " + blobFileName);
                MemoryStream memoryStream = new MemoryStream();
                blobClient.DownloadTo(memoryStream);
                Console.WriteLine("\t Downloaded.");
                // 将状态更新为 processing
                Console.WriteLine("\t Processing in FRE: " + blobFileName);
                task["status"] = "processing";
                queueClient.SendMessage(task.ToString());
                // 使用内存处理方法在 ABBYY FineReader Engine 中处理已下载的 blob
                // 输出结果是在 <ProcessingContainerName> 中保存为 blob 的处理结果名称
                string resultBlobName = "";
                using (FreProcessor.Processor freProcessor = new FreProcessor.Processor())
                {
                    resultBlobName = freProcessor.ProcessBlobFromMemory(memoryStream, blobFileName);
                    Console.WriteLine("\t Result blob name in output container: " + resultBlobName);
                }
                // 删除输入图像
                Console.WriteLine("\t Deleting from input container: " + blobFileName);
                blobClient.Delete();
                // 将状态更新为 succeeded
                // 在 Azure 门户日志中
                Console.WriteLine("Succeeded");
 
                // 在 status-queue 中
                task["status"] = "succeeded";
                task["result-blob-name"] = resultBlobName;
                queueClient.SendMessage(task.ToString());
            }
            catch (Exception error)
            {
                // 如果发生任何错误,则报告
                // 到 Azure 门户日志
                Console.WriteLine("Failed: " + error.Message);
                // 到 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
{
    // 要详细了解 Microsoft Azure WebJobs SDK,请参阅 https://go.microsoft.com/fwlink/?LinkID=320976
    class Program
    {
        // 请在 app.config 中为这些 WebJobs 设置以下连接字符串,以便其运行:
        // AzureWebJobsDashboard 和 AzureWebJobsStorage
        static void Main()
        {
            var config = new JobHostConfiguration();
            if (config.IsDevelopment)
            {
                config.UseDevelopmentSettings();
            }
            // ABBYY FineReader Engine 不是线程安全的,因此我们不能同时处理多条消息
            config.Queues.BatchSize = 1;
            var host = new JobHost(config);
            // 以下代码可确保 WebJob 持续运行
            // 因为其中一个函数已绑定到 Azure 队列,并会侦听新任务
            host.RunAndBlock();
        }
    }
}
using System;
using System.IO;
using System.Runtime.InteropServices;
using FREngine;
namespace FreProcessorJob.FreProcessor
{
    // 用于加载/卸载 FREngine.dll 并初始化/反初始化引擎的类
    // 加载在构造函数中执行,卸载在 Dispose() 中执行
    // 加载失败时抛出异常
    public class EngineLoader : IDisposable
    {
        // 使用存储在 SamplesConfig.cs 中的设置加载 ABBYY FineReader Engine
        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
            {
                // 加载 FREngine.dll 库
                dllHandle = LoadLibraryEx(enginePath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH);
                if (dllHandle == IntPtr.Zero)
                {
                    int error = Marshal.GetLastWin32Error();
                    Console.WriteLine("最后一个 Win32 错误为:" + error);
                    throw new Exception("无法加载 " + enginePath);
                }
 
                IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine");
                if (initializeEnginePtr == IntPtr.Zero)
                {
                    throw new Exception("找不到 InitializeEngine 函数");
                }
                IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine");
                if (deinitializeEnginePtr == IntPtr.Zero)
                {
                    throw new Exception("找不到 DeinitializeEngine 函数");
                }
                IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow");
                if (dllCanUnloadNowPtr == IntPtr.Zero)
                {
                    throw new Exception("找不到 DllCanUnloadNow 函数");
                }
                // 将指针转换为委托
                initializeEngine = (InitializeEngine)Marshal.GetDelegateForFunctionPointer(
                    initializeEnginePtr, typeof(InitializeEngine));
                deinitializeEngine = (DeinitializeEngine)Marshal.GetDelegateForFunctionPointer(
                    deinitializeEnginePtr, typeof(DeinitializeEngine));
                dllCanUnloadNow = (DllCanUnloadNow)Marshal.GetDelegateForFunctionPointer(
                    dllCanUnloadNowPtr, typeof(DllCanUnloadNow));
                // 调用 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)
            {
                // 释放 FREngine.dll 库
                engine = null;
                // 在调用 FreeLibrary 之前释放所有对象
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                FreeLibrary(dllHandle);
                dllHandle = IntPtr.Zero;
                initializeEngine = null;
                deinitializeEngine = null;
                dllCanUnloadNow = null;
                throw;
            }
        }
        // 卸载 ABBYY FineReader Engine
        public void Dispose()
        {
            if (engine == null)
            {
                // 引擎未加载
                return;
            }
            engine = null;
            int hresult = deinitializeEngine();
            // 在调用 FreeLibrary 之前释放所有对象
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            hresult = dllCanUnloadNow();
            if (hresult == 0)
            {
                FreeLibrary(dllHandle);
            }
            dllHandle = IntPtr.Zero;
            initializeEngine = null;
            deinitializeEngine = null;
            dllCanUnloadNow = null;
            // 清理完成后抛出异常
            Marshal.ThrowExceptionForHR(hresult);
        }
        // 返回指向 ABBYY FineReader Engine 主对象的指针
        public IEngine Engine
        {
            get
            {
                return engine;
            }
        }
        // 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);
        // 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();
        // 私有变量
        private FREngine.IEngine engine = null;
        // 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()
        {
            // 在 <ProcessingContainerName> 中创建指向新 blob 的连接。处理结果将存储在其中
            BlobClient resultBlobClient = new BlobClient(Config.ConnectionString, 
                Config.ProcessingContainerName,
                resultBlobName + fileExtension);
            // 覆盖现有文件
            resultBlobClient.DeleteIfExists();
            // 将位置设置为 0,以便从头开始写入文件
            stream.Position = 0;
            resultBlobClient.Upload(stream);
            stream.Close();
        }
        public void Dispose()
        {
            // 在释放对象时关闭内存流,以便在写入数据后能够访问它
            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("正在加载预定义配置文件...");
            // 这是可选的
            engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy");
            // 对于性能较低的 App Service 计划,这是必需的,否则并行处理时会报错
            engineLoader.Engine.MultiProcessingParams.MultiProcessingMode = MultiProcessingModeEnum.MPM_Sequential;
        }
        private void LoadEngine()
        {
            try
            {
                if (engineLoader == null)
                {
                    engineLoader = new EngineLoader();
                }
                setupFREngine();
            }
            catch (Exception error)
            {
                displayMessage("错误: " + error.Message);
            }
        }
        private void UnloadEngine()
        {
            try
            {
                if (engineLoader != null)
                {
                    engineLoader.Dispose();
                    engineLoader = null;
                }
            }
            catch (Exception error)
            {
                displayMessage("错误: " + 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;
 
                // 将图像文件添加到文档中
                displayMessage("正在加载图像...");
                IntPtr handle = Marshal.AllocHGlobal(inputMemoryStream.GetBuffer().Length);
                Marshal.Copy(inputMemoryStream.GetBuffer(), 0, handle, inputMemoryStream.GetBuffer().Length);
                document.AddImageFileFromMemory(handle.ToInt64(), null, null);
                // 识别文档
                displayMessage("正在识别...");
                document.Process(null);
                // 保存结果
                displayMessage("正在保存结果...");
                FileWriter fileWriter = new FileWriter(inputBlobName, ".pdf");
                resultBlobName = inputBlobName + ".pdf";
                document.ExportToMemory(fileWriter, FREngine.FileExportFormatEnum.FEF_PDF, null);
            }
            catch (Exception error)
            {
                displayMessage("错误: " + error.Message);
                throw error;
            }
            finally
            {
                // 关闭文档
                document.Close();
            }
            return resultBlobName;
        }
        public void Dispose()
        {
            UnloadEngine();
        }
    }
}