메인 콘텐츠로 건너뛰기
이 섹션에서는 Windows용 FRE 12 애플리케이션을 Azure App Service에 배포하는 방법을 설명합니다. 예제로는 Azure Storage 계정의 데이터를 사용하는 한 쌍의 WebJob 프로젝트를 사용합니다. 파일 처리는 Blob 컨테이너를 통해 수행됩니다.
이 시나리오를 사용하면 송장, 영수증 등과 같은 작은 단일 페이지 문서에서 최상의 인식 결과를 얻을 수 있습니다.
애플리케이션을 App Service에 배포하는 과정은 여러 단계로 이루어집니다.
  1. 사전 요구 사항에 따라 로컬 컴퓨터와 앱 인스턴스 준비하기
  2. 애플리케이션을 배포하기 전에 사전 준비 단계 수행하기
  3. App Service에서 애플리케이션 배포 및 실행하기
아래 지침에 나와 있는 코드 샘플를 사용하세요.

사전 요구 사항

로컬 컴퓨터

App Service를 만들기 전에 다음 사양에 맞게 로컬 컴퓨터를 준비하세요.
  • Azure용 애플리케이션 개발 모듈이 포함된 Visual Studio 2019(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 래퍼(developer installation 후 C:\ProgramData\ABBYY\SDK\12\FineReader Engine\Inc.NET interops 폴더에 있음)
  • Blob 컨테이너 작업용으로 재정의한 IFileWriter 인터페이스(아래 sample 참조)
  • Azure Storage Explorer(선택 사항 - 여기에서 다운로드)
  • 라이선스 처리 절차에 사용할 Azure 계정의 가상 머신. 이후 구성에는 다음 정보가 필요합니다.
    • IP 주소
    • 열려 있는 연결 포트(기본값 또는 사용자 지정). 포트를 열려면 방화벽을 사용하세요
    • 소켓 네트워크 프로토콜 연결 세부 정보

사전 준비 단계

사전 준비 단계는 로컬 컴퓨터에서 수행해야 합니다. 이 단계를 완료하면 애플리케이션 배포를 시작하는 데 필요한 모든 설정과 파일이 준비됩니다.
  1. ABBYY FineReader Engine 라이브러리를 포함한 아카이브(예: LibraryPackage.zip)를 만듭니다. 파일 목록은 FREngineDistribution.csv 파일에 나와 있습니다.
    중요! 저장 공간이 제한적인 경우(예: 1GB 공간의 App Service Plan을 사용하는 경우), 크기를 최소화한 사용자 지정 ABBYY FineReader Engine 패키지를 만들기 위해 /extract 옵션을 사용하는 것이 좋습니다. 나머지 저장 공간은 파일 처리에 사용됩니다.
    아카이브를 만들 때는 ABBYY FineReader Engine 라이선스 설정이 가상 머신 설정에 맞게 구성되어야 한다는 점을 고려해야 합니다.
    • LicensingSettings.xml 파일은 Network 구성으로 설정되어야 합니다(LicensingSettings.xml 파일 작업 참조).
    • Sockets 네트워크 프로토콜을 사용해야 합니다.
    • 온라인 라이선스 토큰 파일은 Bin64 폴더에 있어야 합니다.
  2. Azure Storage 계정(이 문서에서는 frestorage)을 만듭니다. 필요한 모든 지침은 Azure 웹 사이트에서 확인할 수 있습니다.
  3. 원하는 방식으로 App Service를 만듭니다(여기의 지침 참조).
  4. frestorage 안에 두 개의 Blob 컨테이너를 만듭니다.
    • fre-lib - ABBYY FineReader Engine 파일용
    • processing-container - 처리 결과용
  5. 가장 편한 방법(.NET, Powershell, Python 스크립트 또는 Azure Storage Explorer/Azure Portal 애플리케이션 사용)으로 fre-lib 컨테이너에 LibraryPackage.zip을 업로드합니다.
  6. Azure 계정에서 라이선스 설정이 포함된 가상 머신을 배포하고 구성합니다.
    • LibraryPackage.zip의 installLM.exe를 사용하여 라이선스 관리자 유틸리티를 설치합니다.
    • LicensingSettings.xml에서 Sockets 네트워크 프로토콜을 설정한 다음 Licensing Service를 다시 시작합니다.
    • Azure App Service가 Licensing Service 연결 포트에 액세스할 수 있는지 확인합니다(가상 머신의 Windows 방화벽 규칙 조정).
    • 라이선스를 활성화합니다(Software protection에만 해당하며, Online protection은 활성화가 필요하지 않음).
  7. frestorage 안에 두 개의 대기열을 만듭니다.
    • processing-queue - 파일 처리 작업 등록용
    • status-queue - 작업 완료 알림용
  8. frestorage와 연동하도록 Visual Studio 2019에서 두 개의 Azure WebJob(.NET Framework) 프로젝트를 만듭니다.
    • FreDeployerJob - LibraryPackage.zip을 App Service에 배포하기 위한 프로젝트(파일 목록: Config.cs, Functions.cs, Program.cs는 아래 참조)
    • FreProcessorJob - 문서 처리를 위한 프로젝트(파일 목록: Config.cs, Functions.cs, Program.cs, EngineLoader.cs, IFileWriter.cs, Processor.cs는 아래 참조)

App Service에서 ABBYY FineReader Engine 배포 및 실행

ABBYY FineReader Engine을 배포하려면 다음 단계를 수행합니다.
  1. Visual Studio를 사용해 FreDeployerJob을 Azure App Service에 게시합니다(WebJob Type은 Triggered로 설정).
  2. Azure Portal에서 App Service를 엽니다.
  3. App Service의 WebJobs를 엽니다.
  4. WebJobs 목록에서 FreDeployerJob을 찾습니다.
  5. 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 탭 목록에 표시됩니다. 파일을 처리하려면 다음 단계를 수행합니다.
  1. 처리할 파일을 processing-container에 업로드합니다.
  2. 새 처리 작업에 대한 JSON 메시지를 {“blob-item-name” : “file_name”} 형식으로 processing-queue에 추가합니다. processing-container에 Demo.tif를 업로드한 경우 메시지는 다음과 같아야 합니다.
{"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 하나만 사용하는 방식은 여러 페이지로 구성된 대용량 문서를 처리하는 데 적합하지 않습니다. 이 경우 App Service 대신 Azure Cloud Service 또는 Azure Virtual Machine에서 문서를 인식 처리하는 방안을 고려하세요.

코드 샘플

이 섹션에는 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...");
                        // 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.");
                    }
                }
            }
        }
    }
}
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();
        }
    }
}

FreProcessorJob:

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