本節說明如何將適用於 Windows 的 FRE 12 應用程式部署至 Azure Cloud Service。以下以一個 Worker 為例,示範其如何處理由 Azure Storage 帳戶中容器提供的檔案。
若要實作此案例,會使用連線至 *.abbyy.com 授權伺服器的 線上授權。
ABBYY Licensing Service 同一時間只能搭配一個線上授權運作。
將您的應用程式部署到 Cloud Service 包含數個步驟:
- 依照先決條件設定您的線上授權、本機與雲端執行個體
- 在部署應用程式之前,執行準備步驟
- 將您的應用程式部署到 Cloud Service
請參閱程式碼範例,其中示範如何使用 ABBYY FineReader Engine 的方法來處理文件。
若要使用線上授權,您應向業務團隊取得以下資訊:
若要在安裝 ABBYY Licensing Service 的任何位置使用線上授權,您都必須符合下列條件:
- 可用的網際網路連線
- 允許透過連接埠 443 (HTTPS) 連線至 *.abbyy.com
- 憑證授權單位的 GoDaddy 根憑證 (必須安裝在本機的「受信任的根憑證授權單位」憑證存放區中。憑證詳細資訊請參閱 GoDaddy 網站。)
在建立您的 Cloud Service 之前,請先依照下列規格準備本機環境:
- Visual Studio 2019 及其用於開發 Azure 應用程式的模組 (請查看 Visual Studio 中的 Azure 功能 或使用 Visual Studio Installer 下載這些模組)
- 便於偵錯的 Cloud Service 模擬器與儲存體 (請參閱此處的資訊,並透過 Visual Studio Installer 下載)
- Azure Cloud Service (extended preview) 的範本解決方案,內含單一 Worker 角色專案 (請參閱此處的資訊)
- .NET Framework 4.7.2
- Azure SDK (可在此處下載)
- 用於 Azure Storage 和 Blob 容器的 NuGet 套件 - Azure.Storage.Blobs (可在此處下載)
- 適用於 .NET Framework 4.7 的 ABBYY FineReader Engine 包裝程式 (開發人員安裝後位於 C:\ProgramData\ABBYY\SDK\12\FineReader Engine\Inc.NET interops 資料夾中)
- 已覆寫的 IFileWriter 介面,以便用於 Blob 容器 (請參閱下方的範例)
- Azure Storage Explorer (選用 - 可在此處下載)
Cloud Service 執行個體用於在 Azure 中存放您的 WorkerRole 專案。請依照下列規格設定此執行個體:
- 您的 Service 所使用的 .NET Framework 版本
- 用於部署 ABBYY FineReader Engine 的 PowerShell
- 用於上傳 Azure SDK 的 NuGet 2.8.5.201 或更高版本
- 可透過 PowerShell 搭配 Azure Storage 帳戶使用的 Azure SDK
以下準備步驟需在您的本機電腦上完成。完成這些步驟後,您將備妥開始部署應用程式所需的所有設定和檔案:
- 建立兩個壓縮檔,分別包含 ABBYY FineReader Engine Library 和 Licensing Service (例如 LibraryPackage.zip 和 LSPackage.zip) 。您可以藉助 FREngineDistribution.csv 檔案自動建立檔案清單。請使用來自同一套件的 ABBYY FineReader Engine 和 License Server;否則無法保證相容性。
- 建立 Azure Storage 帳戶 (本文中使用 frestorage) 。您可以在 Azure 網站上找到所有必要的說明。
- 在 frestorage 中建立三個 Blob 容器:
- fre-lib - 用於存放 ABBYY FineReader Engine 檔案
- fre-input - 用於存放輸入檔案
- fre-output - 用於存放處理結果
- 以最方便的方式將 LibraryPackage.zip 和 LSPackage.zip 上傳至 fre-lib 容器 (使用 .NET、Powershell、Python 指令碼或 Azure Storage Explorer/Azure Portal 應用程式) 。
以下以 WorkerRole 專案為例,說明如何在已設定完成的環境中作業。建立專案後,所有必要的組態檔 (.csdef 和 Cloud.cscfg) 都會自動產生。
將 ABBYY FineReader Engine 部署至 Cloud Service
若要將 ABBYY FineReader Engine 部署至新的 WorkerRole 專案:
- 依需求指定 Cloud.cscfg 的 parameters。
- 指定 .csdef 檔案的 parameters:
- (選用) 您的 WorkerRole 設定
- (選用) 虛擬機器大小
- (必要) 角色的本機儲存體 (LocalStorage 區段,在本文中為名為 LocalStorage1 的儲存體) 。請至少設為 3 GB,以確保 ABBYY FineReader Engine 套件能正確部署。
- (必要) 角色的啟動順序 (使用程式碼範例一節中的程式碼範例和設定)
- 實作您的 WorkerRole 專案,以處理來自 fre-input 容器的檔案,並將處理結果發佈至 fre-output 容器:
- 實作 OnStart 方法,讓 Cloud Service 做好執行準備。此方法用於初始化 Engine 並設定 TLS 通訊協定。
- 實作 RunAsync 方法,循環處理從 fre-input 容器取得的檔案。此方法會偵測 fre-input 容器中的檔案、在記憶體中處理這些檔案,然後將其放入 fre-output 容器 (請參閱 Processor.cs、IFileWriter.cs 和 Config.cs) 。
- 實作 OnStop 方法,以完成 Cloud Service 的作業。此方法用於解除初始化 Engine。
本節包含用於設定角色啟動順序及 ABBYY FineReader Engine 設定的程式碼範例:
啟動您的應用程式之前,必須嚴格依照建議順序執行上述指令碼。若要自訂指令碼的執行順序,請指定下列屬性:
- taskType=“simple” - 工作會同步執行,一次執行一個。
- executionContext=“elevated” - 以系統管理員權限執行啟動指令碼 (安裝任何應用程式及執行 LicensingService.exe 時為必要)
因此,系統會建立一個可由 Cloud Service 管理的資料夾。上述指令碼會使用此資料夾來上傳 ABBYY FineReader Engine,並載入 FREngine.dll,以便後續在 sample 中實作。
rem 用於清除先前 ABBYY FineReader Engine 和 Licensing Service 套件的指令碼
echo Cleaning up before launching service >> ".\CleanUpOnStart.log" 2>&1
PowerShell -ExecutionPolicy Unrestricted .\StartupScripts\CleanUpOnStart.ps1 >> ".\CleanUpOnStart.log" 2>&1
echo Cleaning up beafore launching service. >> ".\CleanUpOnStart.log" 2>&1
exit /B %errorlevel%
Write-Host "CleanUpOnStart.ps1 started...";
# 本機儲存體路徑(請參閱 ServiceDefinition.csdef)
$local_storage_name = "LocalStorage1";
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime");
$local_storage_path = ([Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource($local_storage_name)).RootPath.TrimEnd('\\');
# 如果 Licensing Service 處理程序正在執行,則將其停止
$processes = Get-Process;
foreach( $process in $processes ) {
if( $process.Name -eq "LicesingService" ) {
Stop-Process $process;
}
}
# 清理並重新建立 fre_packages 資料夾
$destination_path = Join-Path -Path $local_storage_path -ChildPath 'fre_packages';
if( Test-Path -Path $destination_path ) {
Remove-Item $destination_path -Recurse -Force;
}
New-Item -ItemType Directory -Path $destination_path;
# 清理並重新建立 Input 資料夾
$destination_path = Join-Path -Path $local_storage_path -ChildPath 'Input';
if( Test-Path -Path $destination_path ) {
Remove-Item $destination_path -Recurse -Force;
}
New-Item -ItemType Directory -Path $destination_path;
# 清理並重新建立 Results 資料夾
$destination_path = Join-Path -Path $local_storage_path -ChildPath 'Results';
if( Test-Path -Path $destination_path ) {
Remove-Item $destination_path -Recurse -Force;
}
New-Item -ItemType Directory -Path $destination_path;
Write-Host ("Resource folder was cleaned up.");
rem 用於準備模組的指令碼,以便連線到儲存體帳戶並下載 ABBYY FineReader Engine 和 Licensing Service 套件
echo Preparing PoSh Modules >> ".\PreparePoShModules.log" 2>&1
PowerShell -ExecutionPolicy Unrestricted .\StartupScripts\PreparePoShModules.ps1 >> ".\PreparePoShModules.log" 2>&1
echo PoSh Modules wer prepared. >> ".\PreparePoShModules.log" 2>&1
exit /B %errorlevel%
# 安裝模組
Write-Host "PreparePoShModules.ps1 started...";
# 安裝用於下載 Azure 模組的 NuGet 提供者
Write-Host "Installing NuGet package provider...";
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force;
# 安裝用於操作 Blob 儲體的 Azure Storage 模組
Write-Host "Installing Az.Storage module...";
Install-Module -Name Az.Storage -Scope CurrentUser -Repository PSGallery -Force -AllowClobber;
Write-Host ("PoSh modules were prepared.");
rem 用於準備 ABBYY FineReader Engine 套件的指令碼
echo Preparing Library >> ".\PrepareLibrary.log" 2>&1
PowerShell -ExecutionPolicy Unrestricted .\StartupScripts\PrepareLibrary.ps1 >> ".\PrepareLibrary.log" 2>&1
echo Library was prepared. >> ".\PrepareLibrary.log" 2>&1
exit /B %errorlevel%
Write-Host "PrepareLibrary.ps1 started...";
# 包含 ABBYY FineReader Engine 和 Licensing Service 套件的容器
$container_name = 'fre-lib';
# 儲存體帳戶連線字串
$connection_string = "<connection_string_to_frestorage>";
Write-Host "Configuration";
Write-Host ("Container name: " + $container_name);
Write-Host ("Connection string: " + $storage_account);
# 連線至儲存體物件
Write-Host ("Connecting to storage account...");
$storage_account = New-AzStorageContext -ConnectionString $connection_string;
# 取得 Blob(基本上就是檔案)
Write-Host ("Getting blobs from container...");
$fre_blobs = Get-AzStorageBlob -Container $container_name -Context $storage_account;
# 本機儲存體的路徑(請參閱 ServiceDefinition.csdef)
$local_storage_name = "LocalStorage1";
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime");
$local_storage_path = ([Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource($local_storage_name)).RootPath.TrimEnd('\\');
$destination_path = Join-Path -Path $local_storage_path -ChildPath 'fre_packages';
# 下載唯一需要的 Blob
Write-Host ("Downloading FRE package from container...");
foreach( $blob in $fre_blobs ) {
Write-Host $blob.Name;
if( $blob.Name -eq "LibraryPackage.zip" ) {
Write-Host ("Downloading blob: " + $blob.Name + "to " + $destination_path);
Get-AzStorageBlobContent -Container $container_name -Blob $blob.Name `
-Destination (Join-Path -Path $destination_path -ChildPath $blob.Name) -Context $storage_account;
} else {
Write-Host ("Downloading blob " + $blob.Name + " was skipped.");
}
}
# 解壓縮已下載的套件(將強制覆寫舊版程式庫)
Write-Host ("Unzipping library package...");
Expand-Archive -Path (Join-Path -Path $destination_path -ChildPath "LibraryPackage.zip") `
-DestinationPath (Join-Path -Path $destination_path -ChildPath "LibraryPackage") -Force;
Write-Host ("Library package was prepared.");
rem 用來準備 Licensing Service 套件並啟動 LicensingService.exe 的指令碼
echo Preparing LS >> ".\PrepareLS.log" 2>&1
PowerShell -ExecutionPolicy Unrestricted .\StartupScripts\PrepareLS.ps1 >> ".\PrepareLS.log" 2>&1
echo LS was prepared. >> ".\PrepareLS.log" 2>&1
exit /B %errorlevel%
Write-Host "PrepareLS.ps1 started...";
# 包含 ABBYY FineReader Engine 和 Licensing Service 套件的容器
$container_name = 'fre-lib';
# 儲存體帳戶連線字串
$connection_string = "<connection_string_to_frestorage>";
Write-Host "Configuration";
Write-Host ("Container name: " + $container_name);
Write-Host ("Connection string: " + $storage_account);
# 連線到儲存體物件
Write-Host ("Connecting to storage account...");
$storage_account = New-AzStorageContext -ConnectionString $connection_string;
# 從容器中取得 blob(基本上就是檔案)
Write-Host ("Getting blobs from container...");
$fre_blobs = Get-AzStorageBlob -Container $container_name -Context $storage_account;
# 本機儲存體路徑(請參閱 ServiceDefinition.csdef)
$local_storage_name = "LocalStorage1";
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime");
$local_storage_path = ([Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetLocalResource($local_storage_name)).RootPath.TrimEnd('\\');
$destination_path = Join-Path -Path $local_storage_path -ChildPath 'fre_packages';
# 下載 Licensing Service 套件
Write-Host ("Downloading Licensing Service package from container...");
foreach( $blob in $fre_blobs ) {
Write-Host $blob.Name;
if( $blob.Name -eq "LSPackage.zip" ) {
Write-Host ("Downloading blob: " + $blob.Name + "to " + $destination_path);
Get-AzStorageBlobContent -Container $container_name -Blob $blob.Name `
-Destination (Join-Path -Path $destination_path -ChildPath $blob.Name) -Context $storage_account;
} else {
Write-Host ("Downloading blob " + $blob.Name + " was skipped.");
}
}
# 解壓縮下載的套件
Write-Host ("Unzipping library package...");
Expand-Archive -Path (Join-Path -Path $destination_path -ChildPath "LSPackage.zip") `
-DestinationPath (Join-Path -Path $destination_path -ChildPath "LSPackage") -Force;
# 建立授權資料夾
$program_data_path = [System.Environment]::ExpandEnvironmentVariables("%programdata%");
$path = Join-Path -Path $program_data_path -ChildPath "ABBYY\SDK\12\Licenses";
New-Item -ItemType Directory -Path $path -Force;
# 以獨立模式啟動 Licensing Service
Write-Host ("Starting Licensing Service...");
$licensing_service_app = Join-Path -Path $destination_path -ChildPath "LSPackage\LicensingService.exe";
Start-Process -FilePath $licesing_service_app -ArgumentList "/standalone";
Write-Host ("LS package was prepared and LS was started.");
namespace WorkerRole1.Engine
{
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()
{
// 在 <OutputContainerName> 中建立新 blob 的連線
// 處理結果將儲存在該處
BlobClient resultBlobClient = new BlobClient(Config.ConnectionString, Config.OutputContainerName,
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;
}
}
使用 ABBYY FineReader Engine 處理檔案的範例
namespace WorkerRole1
{
public class WorkerRole : RoleEntryPoint
{
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);
Processor processor = new Processor();
public override void Run()
{
Trace.TraceInformation("WorkerRole1 is running");
try
{
this.RunAsync(this.cancellationTokenSource.Token).Wait();
}
finally
{
this.runCompleteEvent.Set();
}
}
public override bool OnStart()
{
// 設定並行連線數上限
ServicePointManager.DefaultConnectionLimit = 12;
// 將 TLS 設為 1.2,以便連線到儲存體帳戶
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
// 如需有關處理組態變更的資訊,
// 請參閱 https://go.microsoft.com/fwlink/?LinkId=166357 上的 MSDN 主題
bool result = base.OnStart();
Trace.TraceInformation("WorkerRole1 has been started");
processor.LoadEngine();
return result;
}
public override void OnStop()
{
processor.UnloadEngine();
Trace.TraceInformation("WorkerRole1 is stopping");
this.cancellationTokenSource.Cancel();
this.runCompleteEvent.WaitOne();
base.OnStop();
Trace.TraceInformation("WorkerRole1 has stopped");
}
private async Task RunAsync(CancellationToken cancellationToken)
{
// TODO: 請以您自己的邏輯取代下列程式碼
while (!cancellationToken.IsCancellationRequested)
{
Trace.TraceInformation("Working");
try
{
// 建立容器,並傳回容器客戶端物件
BlobContainerClient inputContainerClient = new BlobContainerClient(Config.ConnectionString, Config.InputContainerName);
foreach (BlobItem blobItem in inputContainerClient.GetBlobs())
{
Trace.TraceInformation("\t" + blobItem.Name);
BlobClient blobClient = new BlobClient(Config.ConnectionString, Config.InputContainerName, blobItem.Name);
Trace.TraceInformation("\t Downloading blob to memory: " + blobItem.Name);
MemoryStream memoryStream = new MemoryStream();
blobClient.DownloadTo(memoryStream);
Trace.TraceInformation("\t Processing in FRE: " + blobItem.Name);
string resultBlobName = processor.ProcessImage(memoryStream, blobItem.Name);
Trace.TraceInformation("\t Result blob name in output container: " + resultBlobName);
Trace.TraceInformation("\t Deleting from input container: " + blobItem.Name);
blobClient.Delete();
}
}
catch (Exception error)
{
Trace.TraceInformation("error: " + error.Message);
}
await Task.Delay(1000);
}
}
}
}
namespace WorkerRole1
{
class Processor : IDisposable
{
EngineLoader engineLoader = null;
private void displayMessage(string text)
{
File.AppendAllText(".\\FRE.log", text + "\n");
Trace.TraceInformation("\t" + text );
}
private void loadProfile()
{
engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy");
}
private void setupFREngine()
{
displayMessage("Loading predefined profile...");
loadProfile();
}
public void LoadEngine()
{
try {
if (engineLoader == null)
{
// 與 Hello 範例中使用的是相同的 EngineLoader
engineLoader = new EngineLoader();
}
setupFREngine();
}
catch (Exception error)
{
displayMessage("error: " + error.Message);
}
}
public void UnloadEngine()
{
try
{
if (engineLoader != null)
{
engineLoader.Dispose();
engineLoader = null;
}
}
catch (Exception error)
{
displayMessage("error: " + error.Message);
}
}
public string ProcessImage( MemoryStream inputMemoryStream, string inputBlobName )
{
FRDocument document = engineLoader.Engine.CreateFRDocument();
string resultBlobName = "";
try
{
document.PageFlushingPolicy = FREngine.PageFlushingPolicyEnum.PFP_KeepInMemory;
// 將影像檔加入文件
displayMessage("Loading image...");
IntPtr handle = Marshal.AllocHGlobal(inputMemoryStream.GetBuffer().Length);
Marshal.Copy(inputMemoryStream.GetBuffer(), 0, handle, inputMemoryStream.GetBuffer().Length);
document.AddImageFileFromMemory(handle.ToInt64(), null, null);
// 辨識文件內容
displayMessage("Recognizing...");
document.Process(null);
// 儲存結果
displayMessage("Saving results...");
// 使用 'balanced' 情境將結果儲存為 pdf
// FREngine.PDFExportParams pdfParams = engineLoader.Engine.CreatePDFExportParams();
// pdfParams.Scenario = FREngine.PDFExportScenarioEnum.PES_Balanced;
WorkerRole1.Engine.FileWriter fileWriter = new WorkerRole1.Engine.FileWriter(inputBlobName, ".pdf");
resultBlobName = inputBlobName + ".pdf";
document.ExportToMemory(fileWriter, FREngine.FileExportFormatEnum.FEF_PDF, null);
}
catch (Exception error)
{
displayMessage("error: " + error.Message);
}
finally
{
// 關閉文件
document.Close();
}
return resultBlobName;
}
public void Dispose()
{
UnloadEngine();
}
}
}
class Config
{
// ServiceDefinition.csdef 中定義的本機儲存體
private static string LocalStorageName = "LocalStorage1";
// Blob 容器的連線字串
public static readonly string ConnectionString = "<connection_string_to_frestorage>";
// 儲存體中輸入與輸出容器的名稱
public static readonly string InputContainerName = "fre-input";
public static readonly string OutputContainerName = "fre-output";
// 傳回 ABBYY FineReader Engine 的 Customer Project ID
public static String GetCustomerProjectId()
{
return "<Your_Customer_Project_ID>";
}
// 傳回授權權杖名稱
public static String GetLicenseTokenName()
{
return "<Token_number>.ABBYY.ActivationToken";
}
// 傳回授權密碼
public static String GetLicensePassword()
{
return "<Your_Online_License_token_password>";
}
// 傳回 Engine 資料夾路徑
public static String GetEngineFolder()
{
string engineSubfolder = "fre_packages\\LibraryPackage\\Bin64";
string engineDllFolder = Path.Combine(RoleEnvironment.GetLocalResource(LocalStorageName).RootPath, engineSubfolder);
return engineDllFolder;
}
}