Use this file to discover all available pages before exploring further.
이 섹션에서는 Windows용 FRE 12 애플리케이션을 Azure App Service에 배포하는 방법을 설명합니다. 예제로는 Azure Storage 계정의 데이터를 사용하는 한 쌍의 WebJob 프로젝트를 사용합니다. 파일 처리는 Blob 컨테이너를 통해 수행됩니다. 이 시나리오를 사용하면 송장, 영수증 등과 같은 작은 단일 페이지 문서에서 최상의 인식 결과를 얻을 수 있습니다.애플리케이션을 App Service에 배포하는 과정은 여러 단계로 이루어집니다.
사전 준비 단계는 로컬 컴퓨터에서 수행해야 합니다. 이 단계를 완료하면 애플리케이션 배포를 시작하는 데 필요한 모든 설정과 파일이 준비됩니다.
ABBYY FineReader Engine 라이브러리를 포함한 아카이브(예: LibraryPackage.zip)를 만듭니다. 파일 목록은 FREngineDistribution.csv 파일에 나와 있습니다. 중요! 저장 공간이 제한적인 경우(예: 1GB 공간의 App Service Plan을 사용하는 경우), 크기를 최소화한 사용자 지정 ABBYY FineReader Engine 패키지를 만들기 위해 /extract 옵션을 사용하는 것이 좋습니다. 나머지 저장 공간은 파일 처리에 사용됩니다. 아카이브를 만들 때는 ABBYY FineReader Engine 라이선스 설정이 가상 머신 설정에 맞게 구성되어야 한다는 점을 고려해야 합니다.
WebJobs 탭에서 FreDeployerJob을 마우스 오른쪽 버튼으로 클릭한 다음 Run 명령으로 실행합니다.
배포 결과를 확인하려면 Logs 탭을 열면 됩니다. 배포가 성공하면 LibraryPackage.zip이 fre-lib 컨테이너에서 업로드되어 App Service의 모든 엔터티가 사용할 수 있는 %HOME_EXPANDED% 폴더에 배포됩니다.FreProcessorJob을 배포하려면 Visual Studio를 사용해 FreProcessorJob을 Azure App Service에 게시합니다(WebJob Type은 Continuous로 설정). 그러면 FreProcessorJob이 App Service의 WebJobs 탭 목록에 표시됩니다.파일을 처리하려면 다음 단계를 수행합니다.
처리할 파일을 processing-container에 업로드합니다.
새 처리 작업에 대한 JSON 메시지를 {“blob-item-name” : “file_name”} 형식으로 processing-queue에 추가합니다. processing-container에 Demo.tif를 업로드한 경우 메시지는 다음과 같아야 합니다.
{"blob-item-name" : "Demo.tif"}
작업이 완료될 때까지 기다립니다. 새 작업이 설정되면 즉시 FreProcessorJob이 메모리에서 지정한 파일 처리를 시작합니다. status-queue에는 이 작업의 실행 상태에 대한 항목이 포함됩니다.
processing-container에서 출력 파일을 찾습니다.
FreProcessorJob은 단일 스레드 프로세스로 실행됩니다. 파일을 병렬로 처리하려면 동일한 대기열을 수신하는 FreProcessorJob을 여러 개 만들어야 합니다. 2. FreProcessorJob을 추가할 때마다 메모리를 더 소비합니다. Service Plan을 구매할 때 이 점을 고려하세요. 예를 들어 Azure Free Service Plan에서는 메모리를 적게 사용해 파일 처리의 안정성을 높일 수 있도록 FreProcessorJob 하나만 두는 것이 좋습니다. 3. FreProcessorJob 하나만 사용하는 방식은 여러 페이지로 구성된 대용량 문서를 처리하는 데 적합하지 않습니다. 이 경우 App Service 대신 Azure Cloud Service 또는 Azure Virtual Machine에서 문서를 인식 처리하는 방안을 고려하세요.
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";
Functions.cs
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..."); // temp 폴더를 사용하지 않고 압축 해제(메모리에서만 처리) 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{ // Microsoft Azure WebJobs SDK에 대해 자세히 알아보려면 https://go.microsoft.com/fwlink/?LinkID=320976 를 참조하세요 class Program { // 이 WebJob을 실행하려면 app.config에서 다음 연결 문자열을 설정하세요: // AzureWebJobsDashboard 및 AzureWebJobsStorage static void Main() { // 이 WebJob이 트리거되면 이 메서드만 호출합니다 Functions.DeployFRE(); } }}
using System;using System.IO;namespace FreProcessorJob{ class Config { // blob 컨테이너의 연결 문자열 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"; } // 온라인 라이선스 토큰 이름을 반환합니다 // 온라인 라이선스를 사용하지 않는 경우 빈 string으로 두세요 // 토큰은 라이브러리 패키지의 Bin64 폴더에 있어야 합니다 public static String GetLicenseTokenName() { return "your_online_license_token_if_you_have_it"; } // 온라인 라이선스 password를 반환합니다 // 온라인 라이선스를 사용하지 않는 경우 빈 string으로 두세요 public static String GetLicensePassword() { return "online_license_password_if_you have_it"; } // 라이선스 토큰 이름을 반환합니다 // 라이선싱은 라이브러리 패키지의 Bin64 폴더에 있습니다 public static String GetLicenseTokenName() { return "your_licence_for_ABBYY FineReader Engine"; } // 라이선스 password를 반환합니다 public static String GetLicensePassword() { return "license_password"; } // 엔진 경로를 반환합니다 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 { // 이 함수는 processing-queue라는 Azure Queue에 새 메시지가 기록되면 트리거되어 실행됩니다. // 메시지는 '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 portal의 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()); // 메모리 처리 메서드를 사용해 다운로드한 blob을 ABBYY FineReader Engine에서 처리 // 출력은 <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 portal 로그에 기록 Console.WriteLine("Succeeded"); // status-queue로 전송 task["status"] = "succeeded"; task["result-blob-name"] = resultBlobName; queueClient.SendMessage(task.ToString()); } catch (Exception error) { // 오류가 발생한 경우 보고 // Azure portal 로그로 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()); } } }}
Program.cs
using Microsoft.Azure.WebJobs;namespace FreProcessorJob{ // Microsoft Azure WebJobs SDK에 대해 자세히 알아보려면 https://go.microsoft.com/fwlink/?LinkID=320976 를 참조하세요. class Program { // 이 WebJob이 실행되도록 app.config에 다음 connection string을 설정하세요: // 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(); } }}
EngineLoader.cs
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("The last Win32 Error was: " + error); throw new Exception("Can't load " + enginePath); } IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine"); if (initializeEnginePtr == IntPtr.Zero) { throw new Exception("Can't find InitializeEngine function"); } IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine"); if (deinitializeEnginePtr == IntPtr.Zero) { throw new Exception("Can't find DeinitializeEngine function"); } IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow"); if (dllCanUnloadNowPtr == IntPtr.Zero) { throw new Exception("Can't find DllCanUnloadNow function"); } // 포인터를 델리게이트로 변환 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 변수 private FREngine.IEngine engine = null; // FREngine.dll에 대한 Handle 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() { // <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; }}
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("Loading predefined profile..."); // 이는 선택 사항입니다 engineLoader.Engine.LoadPredefinedProfile("DocumentConversion_Accuracy"); // 성능이 낮은 App Service Plan에서는 병렬 처리 중 오류가 발생할 수 있으므로 이는 필수입니다 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; // 문서에 이미지 파일 추가 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..."); 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 { // 문서 닫기 document.Close(); } return resultBlobName; } public void Dispose() { UnloadEngine(); } }}