푸터 콘텐츠로 바로가기
동영상

AWS Textract에서 IronOCR로 마이그레이션하기

이 가이드는 클라우드 종속성 없이 로컬 문서 처리가 필요한 .NET 개발자를 위해 AWS Textract에서 IronOCR 로의 완전한 마이그레이션 과정을 안내합니다. 이 문서에서는 자격 증명 삭제, 비동기 파이프라인 제거, S3 제거, 그리고 각 Textract 작업을IronOCR의 해당 작업으로 옮기는 데 필요한 특정 코드 교체에 대해 다룹니다.

AWS Textract에서 마이그레이션해야 하는 이유는 무엇일까요?

AWS Textract는 뛰어난 관리형 서비스이지만, 해당 아키텍처로 인해 문서 작업량이 증가함에 따라 재정적 및 운영적 비용이 증가합니다. 본격적인 문서 처리 파이프라인을 구축하는 팀은 결국 이러한 모든 마찰 지점에 직면하게 됩니다.

AWS 자격 증명 인프라는 모든 배포를 복잡하게 만듭니다. Textract 코드를 실행하는 모든 환경에는 AWS 자격 증명이 필요합니다. IAM 사용자 또는 역할, 액세스 키 및 비밀 키, 리전 구성, 그리고 PDF 처리를 위해서는 S3 버킷 권한까지 필요합니다. 첫 페이지를 처리하기 전에 다섯 가지의 개별 구성 단계를 거쳐야 합니다. 프로덕션 배포는 자격 증명 교체 정책, Docker 컨테이너 또는 Kubernetes 포드에 대한 안전한 주입, 그리고 IAM 정책 변화가 조용한 실패를 일으킬 때 AccessDeniedException에 대한 모니터링이 필요합니다.IronOCR라이선스 키라는 문자열 하나만 필요로 하며, 이 키는 시작 시 한 번만 설정하면 됩니다.

페이지당 가격 책정에는 상한선이 없습니다. DetectDocumentText의 페이지당 기본 요율 $0.0015는 최저 가격이며, 비용이 아닙니다. 테이블이 있는 문서는 페이지당 $0.015 AnalyzeDocument를 발생시키며, 기본 요율의 10배입니다. 양식을 추가하면 페이지당 0.05달러가 추가됩니다. 표 추출 기능을 포함한 월 10만 페이지 분량의 혼합 문서 워크플로는 연간 1만 8천 달러의 비용이 발생하며, 이 금액은 매년 1월에 초기화됩니다.IronOCR Professional 라이선스는 2,999달러에 한 번만 구매할 수 있습니다. 그 이후로는 페이지당 비용이 수량에 관계없이 무료입니다.

비동기 PDF 파이프라인은 유지 관리 책임입니다. Textract로 여러 페이지의 PDF를 처리하려면 5개의 개별 단계가 필요합니다: S3 업로드, StartDocumentTextDetection, 폴링 루프, 페이지 매김된 결과 검색, S3 정리. 각각의 단계는 자체 오류 처리, 재시도 전략, 그리고 타임아웃 관리가 필요한 독립적인 실패 모드를 가지고 있습니다. 이 파이프라인을 실제 운영 환경에 배포한 팀들은 구축에 들인 시간보다 유지 관리에 더 많은 시간을 소비한다고 일관되게 보고합니다.IronOCR단 두 줄의 코드로 PDF 파일을 읽습니다.

인터넷 연결이 없으면 Textract를 사용할 수 없습니다. 격리된 네트워크 세그먼트의 Docker 컨테이너, 온프레미스 서버, 에어갭 연구 환경, 외부 트래픽이 제한된 산업 시스템 등 모든 환경에서 Textract를 사용할 수 없다는 공통점이 있습니다.IronOCR NuGet 패키지로 설치되며, 최초 패키지 다운로드 이후에는 네트워크 호출 없이 작동합니다.

모든 호출 시 데이터가 인프라를 벗어납니다. Textract를 사용한 모든 OCR 작업은 문서 콘텐츠를 Amazon 서버로 전송합니다. 개인 건강 정보(PHI)를 처리하는 의료 기관, 통제되지 않은 정보(CUI)를 다루는 방위 산업체, 기밀 통신을 처리하는 법률 팀, 결제 카드 이미지를 처리하는 금융 기관의 경우, 이는 구성 옵션이 아니라 규제 환경에서 Textract를 사용하는 것을 완전히 금지하는 아키텍처적 제약 조건입니다.IronOCR은 하드웨어에서 실행됩니다. 비활성화할 수 있는 클라우드 모드는 없습니다. 로컬 처리만 가능합니다.

속도 제한이 배치 처리량을 제한합니다. 기본 StartDocumentTextDetection TPS 제한은 초당 5개의 요청입니다. 수백 개의 문서를 처리하는 배치 작업에는 SemaphoreSlim 제한, ProvisionedThroughputExceededException에 대한 지수적 백오프, 그리고 속도 보충 타이머가 필요합니다. TPS 증가를 요청하려면 AWS에 공식 지원 사례를 제출해야 하며, 결과가 보장되는 것은 아닙니다. IronOCR는 로컬 CPU에서 허용하는 한 빠르게 처리합니다 — 16코어 서버는 일반 Parallel.ForEach로 16개 문서를 동시에 처리하며, 서비스 계층 협상이 필요 없습니다.

근본적인 문제

Textract 배포는 단 한 페이지도 처리하기 전에 이 의식으로 시작됩니다.

// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call

// Environment must have:
//   AWS_ACCESS_KEY_ID=AKIA...
//   AWS_SECRET_ACCESS_KEY=...
//   AWS_DEFAULT_REGION=us-east-1
//   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
//   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
//   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

public class TextractOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client; // required for PDFs

    public TextractOcrService()
    {
        // Fails with AmazonClientException if credentials not found in chain
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
    }
}
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call

// Environment must have:
//   AWS_ACCESS_KEY_ID=AKIA...
//   AWS_SECRET_ACCESS_KEY=...
//   AWS_DEFAULT_REGION=us-east-1
//   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
//   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
//   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

public class TextractOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client; // required for PDFs

    public TextractOcrService()
    {
        // Fails with AmazonClientException if credentials not found in chain
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
    }
}
Imports Amazon
Imports Amazon.Textract
Imports Amazon.S3

' Textract: five environment variables, an IAM role, and an S3 bucket
' — all required before the first OCR call

' Environment must have:
'   AWS_ACCESS_KEY_ID=AKIA...
'   AWS_SECRET_ACCESS_KEY=...
'   AWS_DEFAULT_REGION=us-east-1
'   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
'   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
'   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

Public Class TextractOcrService
    Private ReadOnly _textractClient As AmazonTextractClient
    Private ReadOnly _s3Client As AmazonS3Client ' required for PDFs

    Public Sub New()
        ' Fails with AmazonClientException if credentials not found in chain
        _textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
        _s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
    End Sub
End Class
$vbLabelText   $csharpLabel

IronOCR 전체 자격 증명 체인을 단일 할당으로 대체합니다.

// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
Imports System

' IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

' Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
$vbLabelText   $csharpLabel

IronOCR과 AWS Textract의 기능 비교

다음 표는 마이그레이션 결정에 가장 중요한 측면에서 두 라이브러리의 기능을 비교한 것입니다.

기능 AWS Textract IronOCR
처리 위치 Amazon 클라우드 (필수) 현지/현장 전용
인터넷 필수 항상 절대
자격 증명 설정 IAM 사용자/역할 + 선택 사항 S3 버킷 단일 라이선스 키 문자열
여러 페이지로 구성된 PDF S3 스테이징 및 비동기 작업이 필요합니다. 직접 동기 input.LoadPdf()
비밀번호로 보호된 PDF 지원되지 않음 input.LoadPdf(path, Password: "x")
멀티프레임 TIFF 지원되지 않음 input.LoadImageFrames("multi.tiff")
비동기 폴링 필요 예 (모든 PDF 파일에 해당) 아니요, 기본적으로 동기식입니다.
속도 제한 기본값 5 TPS (StartDocumentTextDetection) 없음 — CPU 부하만 높음
페이지당 비용 페이지당 $0.0015–$0.065 라이선스 구매 후 $0
자동 전처리 내부 설정이며, 구성할 수 없습니다. 기울기 보정, 노이즈 제거, 대비 조정, 이진화, 선명도 향상, 크기 조정
검색 가능한 PDF 출력 사용 불가 result.SaveAsSearchablePdf()
hOCR 내보내기 사용 불가 result.SaveAsHocrFile()
바코드 판독 사용 불가 ocr.Configuration.ReadBarCodes = true
영역 기반 OCR AnalyzeDocument + 블록 관계를 통해 CropRectangle(x, y, width, height)
구조화된 결과 List<Block> 평면 BlockType 필터링 타입된 Pages, Paragraphs, Lines, Words
지원되는 언어 영어가 주 언어입니다 (추가 항목 선택) NuGet 통해 125개 이상의 언어 팩을 이용할 수 있습니다.
다국어 동시 지원되지 않음 OcrLanguage.French + OcrLanguage.German
에어갭 배포 불가능 완전히 지원
HIPAA (BAA 제외) 불가능 외부 프로세서 미지원 - 온프레미스 전용
Docker 배포 AWS 자격 증명을 주입해야 합니다. 자격 증명 필요 없음 — 일반 NuGet 패키지
크로스 플랫폼 AWS 관리형(리눅스 전용) 윈도우, 리눅스, macOS, Docker, Azure, AWS EC2
라이센스 모델 페이지당 사용량 기반 요금 청구(지속적인 사용량 기준) 영구 일회성 ($999 / $1,499 / $2,999)

빠른 시작: AWS Textract에서IronOCR로 마이그레이션

1단계: NuGet 패키지 교체

AWS SDK 패키지를 제거하세요.

dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
SHELL

NuGet 에서IronOCR설치하세요.

dotnet add package IronOcr

단계 2: 네임스페이스 업데이트

모든 AWS 네임스페이스 가져오기를 단일IronOCR네임스페이스로 대체합니다.

// Before (AWS Textract)
using Amazon.Textract;
using Amazon.Textract.Model;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;

// After (IronOCR)
using IronOcr;
// Before (AWS Textract)
using Amazon.Textract;
using Amazon.Textract.Model;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;

// After (IronOCR)
using IronOcr;
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Runtime

Imports IronOcr
$vbLabelText   $csharpLabel

단계 3: 라이선스 초기화

애플리케이션 시작 시 라이선스 초기화를 한 번만 수행하도록 추가합니다. 모든 AWS 자격 증명 환경 변수 설정을 제거합니다.

// Remove from startup:
//   AWS_ACCESS_KEY_ID environment variable
//   AWS_SECRET_ACCESS_KEY environment variable
//   AWS_DEFAULT_REGION environment variable
//   ~/.aws/credentials file entries
//   IAM role bindings on the execution context

// Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Remove from startup:
//   AWS_ACCESS_KEY_ID environment variable
//   AWS_SECRET_ACCESS_KEY environment variable
//   AWS_DEFAULT_REGION environment variable
//   ~/.aws/credentials file entries
//   IAM role bindings on the execution context

// Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
' Remove from startup:
'   AWS_ACCESS_KEY_ID environment variable
'   AWS_SECRET_ACCESS_KEY environment variable
'   AWS_DEFAULT_REGION environment variable
'   ~/.aws/credentials file entries
'   IAM role bindings on the execution context

' Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
$vbLabelText   $csharpLabel

코드 마이그레이션 예제

AmazonTextractClient 생성자 및 IAM 설정 교체

마이그레이션에서 가장 눈에 띄는 변화는 클라이언트 초기화입니다. Textract는 자격 증명 확인을 기반으로 하는 종속성 주입 클라이언트를 필요로 합니다. 즉, 클라이언트와 모든 종속성이 모든 배포 환경에 미리 구성되어 있어야 합니다. 잘못된 구성은 첫 번째 OCR 호출이 AmazonClientException를 던질 때까지 조용히 실패합니다.

AWS Textract 접근 방식:

// Requires: NuGet AWSSDK.Textract, AWSSDK.S3
// Requires: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION in environment
// Requires: IAM policy with textract:* and s3:PutObject, s3:DeleteObject

using Amazon.S3;
using Amazon.Textract;

public class DocumentOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client;
    private readonly string _stagingBucket;

    public DocumentOcrService(IConfiguration config)
    {
        // Credential chain: environment → ~/.aws/credentials → EC2 instance profile
        // Fails if none found — runtime exception, not compile-time
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
        _stagingBucket = config["Textract:StagingBucket"]; // bucket must exist and have permissions
    }

    public async Task<string> ReadImageAsync(string imagePath)
    {
        var bytes = File.ReadAllBytes(imagePath);
        var response = await _textractClient.DetectDocumentTextAsync(
            new DetectDocumentTextRequest
            {
                Document = new Document { Bytes = new MemoryStream(bytes) }
            });

        return string.Join("\n", response.Blocks
            .Where(b => b.BlockType == BlockType.LINE)
            .Select(b => b.Text));
    }
}
// Requires: NuGet AWSSDK.Textract, AWSSDK.S3
// Requires: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION in environment
// Requires: IAM policy with textract:* and s3:PutObject, s3:DeleteObject

using Amazon.S3;
using Amazon.Textract;

public class DocumentOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client;
    private readonly string _stagingBucket;

    public DocumentOcrService(IConfiguration config)
    {
        // Credential chain: environment → ~/.aws/credentials → EC2 instance profile
        // Fails if none found — runtime exception, not compile-time
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
        _stagingBucket = config["Textract:StagingBucket"]; // bucket must exist and have permissions
    }

    public async Task<string> ReadImageAsync(string imagePath)
    {
        var bytes = File.ReadAllBytes(imagePath);
        var response = await _textractClient.DetectDocumentTextAsync(
            new DetectDocumentTextRequest
            {
                Document = new Document { Bytes = new MemoryStream(bytes) }
            });

        return string.Join("\n", response.Blocks
            .Where(b => b.BlockType == BlockType.LINE)
            .Select(b => b.Text));
    }
}
Imports Amazon.S3
Imports Amazon.Textract
Imports System.IO
Imports System.Threading.Tasks
Imports Microsoft.Extensions.Configuration

Public Class DocumentOcrService
    Private ReadOnly _textractClient As AmazonTextractClient
    Private ReadOnly _s3Client As AmazonS3Client
    Private ReadOnly _stagingBucket As String

    Public Sub New(config As IConfiguration)
        ' Credential chain: environment → ~/.aws/credentials → EC2 instance profile
        ' Fails if none found — runtime exception, not compile-time
        _textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
        _s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
        _stagingBucket = config("Textract:StagingBucket") ' bucket must exist and have permissions
    End Sub

    Public Async Function ReadImageAsync(imagePath As String) As Task(Of String)
        Dim bytes = File.ReadAllBytes(imagePath)
        Dim response = Await _textractClient.DetectDocumentTextAsync(
            New DetectDocumentTextRequest With {
                .Document = New Document With {.Bytes = New MemoryStream(bytes)}
            })

        Return String.Join(vbCrLf, response.Blocks _
            .Where(Function(b) b.BlockType = BlockType.LINE) _
            .Select(Function(b) b.Text))
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// Requires: NuGet IronOcr
// Requires: one license key string — nothing else

using IronOcr;

public class DocumentOcrService
{
    private readonly IronTesseract _ocr;

    public DocumentOcrService()
    {
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
        _ocr = new IronTesseract();
    }

    public string ReadImage(string imagePath)
    {
        // No credential chain, no region, no bucket — just read
        return _ocr.Read(imagePath).Text;
    }
}
// Requires: NuGet IronOcr
// Requires: one license key string — nothing else

using IronOcr;

public class DocumentOcrService
{
    private readonly IronTesseract _ocr;

    public DocumentOcrService()
    {
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
        _ocr = new IronTesseract();
    }

    public string ReadImage(string imagePath)
    {
        // No credential chain, no region, no bucket — just read
        return _ocr.Read(imagePath).Text;
    }
}
Imports IronOcr

Public Class DocumentOcrService
    Private ReadOnly _ocr As IronTesseract

    Public Sub New()
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY" ' or set at Program.vb startup
        _ocr = New IronTesseract()
    End Sub

    Public Function ReadImage(imagePath As String) As String
        ' No credential chain, no region, no bucket — just read
        Return _ocr.Read(imagePath).Text
    End Function
End Class
$vbLabelText   $csharpLabel

전체 AWS 자격 증명 인프라 — IAM 역할, 환경 변수, ~/.aws/credentials 파일, S3 버킷 수명주기 정책, 지역 구성 — 제거됩니다. DocumentOcrService 생성자는 런타임 실패 모드를 가진 3개의 종속성 주입 사이트에서 단일 라이선스 할당으로 변경됩니다. 배포 패턴에 대한 자세한 내용은 IronTesseract 설정 가이드를 참조하십시오.

StartDocumentTextDetection을 TIFF 다중 프레임 처리로 대체

Textract의 비동기 작업 API (StartDocumentTextDetection)는 다중 페이지 문서에 필요합니다. 문서 디지털화 파이프라인에서 흔히 볼 수 있는 패턴은 스캔한 문서를 페이지당 하나의 프레임으로 구성된 멀티프레임 TIFF 파일로 받는 것입니다. Textract는 TIFF 입력을 직접 받지 않습니다. 작업을 시작하기 전에 문서를 PDF로 변환하여 S3에 업로드해야 합니다.IronOCR멀티프레임 TIFF 파일을 기본적으로 읽습니다.

AWS Textract 접근 방식:

// Textract cannot accept TIFF directly — requires PDF conversion + S3 + async job
// This shows the conversion overhead plus the minimum async pipeline

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<string> ProcessScannedTiffAsync(string tiffPath, string s3Bucket)
{
    // Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
    // Requires a separate PDF conversion library — not shown here
    var pdfPath = Path.ChangeExtension(tiffPath, ".pdf");
    ConvertTiffToPdf(tiffPath, pdfPath); // external dependency required

    // Step 1: Upload converted PDF to S3
    var s3Key = $"staging/{Guid.NewGuid()}.pdf";
    using (var stream = File.OpenRead(pdfPath))
    {
        await _s3Client.PutObjectAsync(new PutObjectRequest
        {
            BucketName = s3Bucket,
            Key = s3Key,
            InputStream = stream
        });
    }

    try
    {
        // Step 2: Start async detection job
        var startResp = await _textractClient.StartDocumentTextDetectionAsync(
            new StartDocumentTextDetectionRequest
            {
                DocumentLocation = new DocumentLocation
                {
                    S3Object = new S3Object { Bucket = s3Bucket, Name = s3Key }
                }
            });

        // Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
        GetDocumentTextDetectionResponse pollResp;
        do
        {
            await Task.Delay(5000);
            pollResp = await _textractClient.GetDocumentTextDetectionAsync(
                new GetDocumentTextDetectionRequest { JobId = startResp.JobId });
        } while (pollResp.JobStatus == JobStatus.IN_PROGRESS);

        if (pollResp.JobStatus != JobStatus.SUCCEEDED)
            throw new Exception($"Job failed: {pollResp.StatusMessage}");

        // Step 4: Collect paginated results
        var text = new StringBuilder();
        string token = null;
        do
        {
            var page = await _textractClient.GetDocumentTextDetectionAsync(
                new GetDocumentTextDetectionRequest
                {
                    JobId = startResp.JobId,
                    NextToken = token
                });
            foreach (var block in page.Blocks.Where(b => b.BlockType == BlockType.LINE))
                text.AppendLine(block.Text);
            token = page.NextToken;
        } while (token != null);

        return text.ToString();
    }
    finally
    {
        // Step 5: Clean up S3 — orphaned objects accumulate storage costs
        await _s3Client.DeleteObjectAsync(s3Bucket, s3Key);
        File.Delete(pdfPath); // clean up intermediate PDF
    }
}
// Textract cannot accept TIFF directly — requires PDF conversion + S3 + async job
// This shows the conversion overhead plus the minimum async pipeline

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<string> ProcessScannedTiffAsync(string tiffPath, string s3Bucket)
{
    // Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
    // Requires a separate PDF conversion library — not shown here
    var pdfPath = Path.ChangeExtension(tiffPath, ".pdf");
    ConvertTiffToPdf(tiffPath, pdfPath); // external dependency required

    // Step 1: Upload converted PDF to S3
    var s3Key = $"staging/{Guid.NewGuid()}.pdf";
    using (var stream = File.OpenRead(pdfPath))
    {
        await _s3Client.PutObjectAsync(new PutObjectRequest
        {
            BucketName = s3Bucket,
            Key = s3Key,
            InputStream = stream
        });
    }

    try
    {
        // Step 2: Start async detection job
        var startResp = await _textractClient.StartDocumentTextDetectionAsync(
            new StartDocumentTextDetectionRequest
            {
                DocumentLocation = new DocumentLocation
                {
                    S3Object = new S3Object { Bucket = s3Bucket, Name = s3Key }
                }
            });

        // Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
        GetDocumentTextDetectionResponse pollResp;
        do
        {
            await Task.Delay(5000);
            pollResp = await _textractClient.GetDocumentTextDetectionAsync(
                new GetDocumentTextDetectionRequest { JobId = startResp.JobId });
        } while (pollResp.JobStatus == JobStatus.IN_PROGRESS);

        if (pollResp.JobStatus != JobStatus.SUCCEEDED)
            throw new Exception($"Job failed: {pollResp.StatusMessage}");

        // Step 4: Collect paginated results
        var text = new StringBuilder();
        string token = null;
        do
        {
            var page = await _textractClient.GetDocumentTextDetectionAsync(
                new GetDocumentTextDetectionRequest
                {
                    JobId = startResp.JobId,
                    NextToken = token
                });
            foreach (var block in page.Blocks.Where(b => b.BlockType == BlockType.LINE))
                text.AppendLine(block.Text);
            token = page.NextToken;
        } while (token != null);

        return text.ToString();
    }
    finally
    {
        // Step 5: Clean up S3 — orphaned objects accumulate storage costs
        await _s3Client.DeleteObjectAsync(s3Bucket, s3Key);
        File.Delete(pdfPath); // clean up intermediate PDF
    }
}
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Text
Imports System.Threading.Tasks

Public Async Function ProcessScannedTiffAsync(tiffPath As String, s3Bucket As String) As Task(Of String)
    ' Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
    ' Requires a separate PDF conversion library — not shown here
    Dim pdfPath = Path.ChangeExtension(tiffPath, ".pdf")
    ConvertTiffToPdf(tiffPath, pdfPath) ' external dependency required

    ' Step 1: Upload converted PDF to S3
    Dim s3Key = $"staging/{Guid.NewGuid()}.pdf"
    Using stream = File.OpenRead(pdfPath)
        Await _s3Client.PutObjectAsync(New PutObjectRequest With {
            .BucketName = s3Bucket,
            .Key = s3Key,
            .InputStream = stream
        })
    End Using

    Try
        ' Step 2: Start async detection job
        Dim startResp = Await _textractClient.StartDocumentTextDetectionAsync(
            New StartDocumentTextDetectionRequest With {
                .DocumentLocation = New DocumentLocation With {
                    .S3Object = New S3Object With {.Bucket = s3Bucket, .Name = s3Key}
                }
            })

        ' Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
        Dim pollResp As GetDocumentTextDetectionResponse
        Do
            Await Task.Delay(5000)
            pollResp = Await _textractClient.GetDocumentTextDetectionAsync(
                New GetDocumentTextDetectionRequest With {.JobId = startResp.JobId})
        Loop While pollResp.JobStatus = JobStatus.IN_PROGRESS

        If pollResp.JobStatus <> JobStatus.SUCCEEDED Then
            Throw New Exception($"Job failed: {pollResp.StatusMessage}")
        End If

        ' Step 4: Collect paginated results
        Dim text = New StringBuilder()
        Dim token As String = Nothing
        Do
            Dim page = Await _textractClient.GetDocumentTextDetectionAsync(
                New GetDocumentTextDetectionRequest With {
                    .JobId = startResp.JobId,
                    .NextToken = token
                })
            For Each block In page.Blocks.Where(Function(b) b.BlockType = BlockType.LINE)
                text.AppendLine(block.Text)
            Next
            token = page.NextToken
        Loop While token IsNot Nothing

        Return text.ToString()
    Finally
        ' Step 5: Clean up S3 — orphaned objects accumulate storage costs
        Await _s3Client.DeleteObjectAsync(s3Bucket, s3Key)
        File.Delete(pdfPath) ' clean up intermediate PDF
    End Try
End Function
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

//IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling

using IronOcr;

public string ProcessScannedTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath); // reads all frames natively

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    // Access per-page results if needed
    foreach (var page in result.Pages)
        Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");

    return result.Text;
}
//IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling

using IronOcr;

public string ProcessScannedTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath); // reads all frames natively

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    // Access per-page results if needed
    foreach (var page in result.Pages)
        Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");

    return result.Text;
}
Imports IronOcr

Public Function ProcessScannedTiff(tiffPath As String) As String
    Using input As New OcrInput()
        input.LoadImageFrames(tiffPath) ' reads all frames natively

        Dim ocr As New IronTesseract()
        Dim result = ocr.Read(input)

        ' Access per-page results if needed
        For Each page In result.Pages
            Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected")
        Next

        Return result.Text
    End Using
End Function
$vbLabelText   $csharpLabel

TIFF를 PDF로 변환하는 단계, S3 업로드, 비동기 폴링 루프, 페이지별 결과 누적 및 S3 정리 작업이 모두 제거되었습니다.IronOCR구현은 프레임을 직접 읽고 중간 형식 변환 없이 result.Pages를 통해 페이지별 접근을 제공합니다. TIFF 및 GIF 입력 가이드 에서는 일부 프레임만 필요한 경우 프레임 선택에 대해 설명합니다.

S3 스테이징을 검색 가능한 PDF 출력으로 대체

Textract의 일반적인 사용 사례는 스캔한 PDF 파일을 검색 가능한 형식으로 변환하는 것입니다. Textract 방식은 각 PDF 파일을 S3에 업로드하고, 비동기 작업을 실행하고, 추출된 텍스트를 수집한 다음, 별도의 PDF 라이브러리를 사용하여 해당 텍스트를 검색 가능한 레이어로 삽입하는 과정을 필요로 합니다.IronOCR단 한 번의 호출로 스캔을 수행하고 검색 가능한 PDF 파일을 생성합니다.

AWS Textract 접근 방식:

// Textract extracts text but cannot produce searchable PDFs
// Requires: S3 upload + async job + separate PDF library to embed the text layer

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<string> ExtractTextForSearchableLayerAsync(
    string scannedPdfPath,
    string s3Bucket)
{
    // Must upload to S3 — cannot pass PDF bytes directly for async jobs
    var key = $"ocr-input/{Guid.NewGuid()}.pdf";

    await using (var fs = File.OpenRead(scannedPdfPath))
    {
        await _s3Client.PutObjectAsync(new PutObjectRequest
        {
            BucketName = s3Bucket,
            Key = key,
            InputStream = fs,
            ContentType = "application/pdf"
        });
    }

    string jobId;
    try
    {
        var startResp = await _textractClient.StartDocumentTextDetectionAsync(
            new StartDocumentTextDetectionRequest
            {
                DocumentLocation = new DocumentLocation
                {
                    S3Object = new S3Object { Bucket = s3Bucket, Name = key }
                }
            });
        jobId = startResp.JobId;
    }
    catch
    {
        await _s3Client.DeleteObjectAsync(s3Bucket, key);
        throw;
    }

    // Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
    GetDocumentTextDetectionResponse pollResp;
    int pollAttempts = 0;
    const int maxAttempts = 120; // 10 minute timeout

    do
    {
        await Task.Delay(5000);
        pollResp = await _textractClient.GetDocumentTextDetectionAsync(
            new GetDocumentTextDetectionRequest { JobId = jobId });

        if (++pollAttempts >= maxAttempts)
            throw new TimeoutException("Textract job timed out after 10 minutes");

    } while (pollResp.JobStatus == JobStatus.IN_PROGRESS);

    await _s3Client.DeleteObjectAsync(s3Bucket, key); // cleanup regardless of outcome

    if (pollResp.JobStatus != JobStatus.SUCCEEDED)
        throw new Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}");

    // Return extracted text — caller must use a separate library to produce searchable PDF
    return string.Join("\n", pollResp.Blocks
        .Where(b => b.BlockType == BlockType.LINE)
        .Select(b => b.Text));

    // Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
}
// Textract extracts text but cannot produce searchable PDFs
// Requires: S3 upload + async job + separate PDF library to embed the text layer

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<string> ExtractTextForSearchableLayerAsync(
    string scannedPdfPath,
    string s3Bucket)
{
    // Must upload to S3 — cannot pass PDF bytes directly for async jobs
    var key = $"ocr-input/{Guid.NewGuid()}.pdf";

    await using (var fs = File.OpenRead(scannedPdfPath))
    {
        await _s3Client.PutObjectAsync(new PutObjectRequest
        {
            BucketName = s3Bucket,
            Key = key,
            InputStream = fs,
            ContentType = "application/pdf"
        });
    }

    string jobId;
    try
    {
        var startResp = await _textractClient.StartDocumentTextDetectionAsync(
            new StartDocumentTextDetectionRequest
            {
                DocumentLocation = new DocumentLocation
                {
                    S3Object = new S3Object { Bucket = s3Bucket, Name = key }
                }
            });
        jobId = startResp.JobId;
    }
    catch
    {
        await _s3Client.DeleteObjectAsync(s3Bucket, key);
        throw;
    }

    // Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
    GetDocumentTextDetectionResponse pollResp;
    int pollAttempts = 0;
    const int maxAttempts = 120; // 10 minute timeout

    do
    {
        await Task.Delay(5000);
        pollResp = await _textractClient.GetDocumentTextDetectionAsync(
            new GetDocumentTextDetectionRequest { JobId = jobId });

        if (++pollAttempts >= maxAttempts)
            throw new TimeoutException("Textract job timed out after 10 minutes");

    } while (pollResp.JobStatus == JobStatus.IN_PROGRESS);

    await _s3Client.DeleteObjectAsync(s3Bucket, key); // cleanup regardless of outcome

    if (pollResp.JobStatus != JobStatus.SUCCEEDED)
        throw new Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}");

    // Return extracted text — caller must use a separate library to produce searchable PDF
    return string.Join("\n", pollResp.Blocks
        .Where(b => b.BlockType == BlockType.LINE)
        .Select(b => b.Text));

    // Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
}
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Textract
Imports Amazon.Textract.Model

Public Async Function ExtractTextForSearchableLayerAsync(
    scannedPdfPath As String,
    s3Bucket As String) As Task(Of String)

    ' Must upload to S3 — cannot pass PDF bytes directly for async jobs
    Dim key = $"ocr-input/{Guid.NewGuid()}.pdf"

    Await Using fs = File.OpenRead(scannedPdfPath)
        Await _s3Client.PutObjectAsync(New PutObjectRequest With {
            .BucketName = s3Bucket,
            .Key = key,
            .InputStream = fs,
            .ContentType = "application/pdf"
        })
    End Using

    Dim jobId As String
    Try
        Dim startResp = Await _textractClient.StartDocumentTextDetectionAsync(
            New StartDocumentTextDetectionRequest With {
                .DocumentLocation = New DocumentLocation With {
                    .S3Object = New S3Object With {.Bucket = s3Bucket, .Name = key}
                }
            })
        jobId = startResp.JobId
    Catch
        Await _s3Client.DeleteObjectAsync(s3Bucket, key)
        Throw
    End Try

    ' Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
    Dim pollResp As GetDocumentTextDetectionResponse
    Dim pollAttempts As Integer = 0
    Const maxAttempts As Integer = 120 ' 10 minute timeout

    Do
        Await Task.Delay(5000)
        pollResp = Await _textractClient.GetDocumentTextDetectionAsync(
            New GetDocumentTextDetectionRequest With {.JobId = jobId})

        If System.Threading.Interlocked.Increment(pollAttempts) >= maxAttempts Then
            Throw New TimeoutException("Textract job timed out after 10 minutes")
        End If

    Loop While pollResp.JobStatus = JobStatus.IN_PROGRESS

    Await _s3Client.DeleteObjectAsync(s3Bucket, key) ' cleanup regardless of outcome

    If pollResp.JobStatus <> JobStatus.SUCCEEDED Then
        Throw New Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}")
    End If

    ' Return extracted text — caller must use a separate library to produce searchable PDF
    Return String.Join(vbLf, pollResp.Blocks _
        .Where(Function(b) b.BlockType = BlockType.LINE) _
        .Select(Function(b) b.Text))

    ' Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
End Function
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

//IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed

using IronOcr;

public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
    var ocr = new IronTesseract();

    using var input = new OcrInput();
    input.LoadPdf(scannedPdfPath);

    var result = ocr.Read(input);

    // Embed extracted text as searchable layer in one call
    result.SaveAsSearchablePdf(outputPath);

    Console.WriteLine($"Pages processed: {result.Pages.Length}");
    Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
//IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed

using IronOcr;

public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
    var ocr = new IronTesseract();

    using var input = new OcrInput();
    input.LoadPdf(scannedPdfPath);

    var result = ocr.Read(input);

    // Embed extracted text as searchable layer in one call
    result.SaveAsSearchablePdf(outputPath);

    Console.WriteLine($"Pages processed: {result.Pages.Length}");
    Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
Imports IronOcr

Public Sub ConvertToSearchablePdf(scannedPdfPath As String, outputPath As String)
    Dim ocr As New IronTesseract()

    Using input As New OcrInput()
        input.LoadPdf(scannedPdfPath)

        Dim result = ocr.Read(input)

        ' Embed extracted text as searchable layer in one call
        result.SaveAsSearchablePdf(outputPath)

        Console.WriteLine($"Pages processed: {result.Pages.Length}")
        Console.WriteLine($"Overall confidence: {result.Confidence:F1}%")
    End Using
End Sub
$vbLabelText   $csharpLabel

폴링 타임아웃 로직, S3 업로드 및 정리 패턴, 외부 PDF 라이브러리 의존성이 모두 제거되었습니다. SaveAsSearchablePdf는 인식된 텍스트를 출력 파일에 직접 포함시켜, 모든 스캔된 페이지가 두 번째 라이브러리 없이 전체 텍스트 검색이 가능하게 합니다. 검색 가능한 PDF 가이드에는 페이지 선택, 압축 옵션 및 메타데이터 삽입에 대한 내용이 포함되어 있습니다. PDF OCR 파이프라인의 더 넓은 맥락에 대해서는 PDF 입력 가이드를 참조하십시오.

AnalyzeDocument 블록 그래프 탐색을 구조화된 페이지 결과로 대체

Textract의 AnalyzeDocumentTABLESFORMS와 함께 각 요소 — 줄, 단어, 셀, 테이블 헤더, 폼 키, 폼 값 — 가 같은 List<Block> 유형을 가진 평면 Block를 반환하며, BlockType에 의해 구분되고 RelationshipType.CHILD ID 배열에 의해 연결됩니다. 문서의 논리적 구조를 재구성하려면 이러한 관계 그래프를 탐색해야 합니다.IronOCR페이지, 단락, 줄, 단어 등 각 항목에 대한 좌표 접근 권한을 포함하는 계층 구조를 반환합니다.

AWS Textract 접근 방식:

// AnalyzeDocument with TABLES returns blocks that must be graph-traversed
// to determine which words belong to which paragraphs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<List<DocumentSection>> ExtractDocumentStructureAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "TABLES", "FORMS" }
            // Note: AnalyzeDocument (TABLES) costs $0.015/page — 10x DetectDocumentText
        });

    var sections = new List<DocumentSection>();

    // Filter LINE blocks for paragraph reconstruction
    // LINE blocks are not grouped into paragraphs — must infer from Y proximity
    var lineBlocks = response.Blocks
        .Where(b => b.BlockType == BlockType.LINE)
        .OrderBy(b => b.Geometry?.BoundingBox?.Top ?? 0)
        .ToList();

    // Group lines into pseudo-paragraphs by vertical gap heuristic
    var currentParagraph = new List<Block>();
    float? lastBottom = null;
    const float paragraphGapThreshold = 0.02f; // 2% of page height

    foreach (var line in lineBlocks)
    {
        var top = line.Geometry?.BoundingBox?.Top ?? 0;
        var height = line.Geometry?.BoundingBox?.Height ?? 0;

        if (lastBottom.HasValue && (top - lastBottom.Value) > paragraphGapThreshold)
        {
            if (currentParagraph.Any())
            {
                sections.Add(new DocumentSection
                {
                    Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
                    LineCount = currentParagraph.Count,
                    // Confidence: average of all LINE block confidence values
                    Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
                });
                currentParagraph.Clear();
            }
        }

        currentParagraph.Add(line);
        lastBottom = top + height;
    }

    if (currentParagraph.Any())
        sections.Add(new DocumentSection
        {
            Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
            LineCount = currentParagraph.Count,
            Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
        });

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public float Confidence { get; set; }
}
// AnalyzeDocument with TABLES returns blocks that must be graph-traversed
// to determine which words belong to which paragraphs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<List<DocumentSection>> ExtractDocumentStructureAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "TABLES", "FORMS" }
            // Note: AnalyzeDocument (TABLES) costs $0.015/page — 10x DetectDocumentText
        });

    var sections = new List<DocumentSection>();

    // Filter LINE blocks for paragraph reconstruction
    // LINE blocks are not grouped into paragraphs — must infer from Y proximity
    var lineBlocks = response.Blocks
        .Where(b => b.BlockType == BlockType.LINE)
        .OrderBy(b => b.Geometry?.BoundingBox?.Top ?? 0)
        .ToList();

    // Group lines into pseudo-paragraphs by vertical gap heuristic
    var currentParagraph = new List<Block>();
    float? lastBottom = null;
    const float paragraphGapThreshold = 0.02f; // 2% of page height

    foreach (var line in lineBlocks)
    {
        var top = line.Geometry?.BoundingBox?.Top ?? 0;
        var height = line.Geometry?.BoundingBox?.Height ?? 0;

        if (lastBottom.HasValue && (top - lastBottom.Value) > paragraphGapThreshold)
        {
            if (currentParagraph.Any())
            {
                sections.Add(new DocumentSection
                {
                    Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
                    LineCount = currentParagraph.Count,
                    // Confidence: average of all LINE block confidence values
                    Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
                });
                currentParagraph.Clear();
            }
        }

        currentParagraph.Add(line);
        lastBottom = top + height;
    }

    if (currentParagraph.Any())
        sections.Add(new DocumentSection
        {
            Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
            LineCount = currentParagraph.Count,
            Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
        });

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public float Confidence { get; set; }
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Linq
Imports System.Threading.Tasks

Public Class DocumentProcessor
    Private _textractClient As IAmazonTextract

    Public Sub New(textractClient As IAmazonTextract)
        _textractClient = textractClient
    End Sub

    Public Async Function ExtractDocumentStructureAsync(imagePath As String) As Task(Of List(Of DocumentSection))
        Dim bytes = File.ReadAllBytes(imagePath)

        Dim response = Await _textractClient.AnalyzeDocumentAsync(
            New AnalyzeDocumentRequest With {
                .Document = New Document With {.Bytes = New MemoryStream(bytes)},
                .FeatureTypes = New List(Of String) From {"TABLES", "FORMS"}
            })

        Dim sections = New List(Of DocumentSection)()

        Dim lineBlocks = response.Blocks _
            .Where(Function(b) b.BlockType = BlockType.LINE) _
            .OrderBy(Function(b) If(b.Geometry?.BoundingBox?.Top, 0)) _
            .ToList()

        Dim currentParagraph = New List(Of Block)()
        Dim lastBottom As Single? = Nothing
        Const paragraphGapThreshold As Single = 0.02F

        For Each line In lineBlocks
            Dim top = If(line.Geometry?.BoundingBox?.Top, 0)
            Dim height = If(line.Geometry?.BoundingBox?.Height, 0)

            If lastBottom.HasValue AndAlso (top - lastBottom.Value) > paragraphGapThreshold Then
                If currentParagraph.Any() Then
                    sections.Add(New DocumentSection With {
                        .Text = String.Join(" ", currentParagraph.Select(Function(b) b.Text)),
                        .LineCount = currentParagraph.Count,
                        .Confidence = CSng(currentParagraph.Average(Function(b) If(b.Confidence, 0)))
                    })
                    currentParagraph.Clear()
                End If
            End If

            currentParagraph.Add(line)
            lastBottom = top + height
        Next

        If currentParagraph.Any() Then
            sections.Add(New DocumentSection With {
                .Text = String.Join(" ", currentParagraph.Select(Function(b) b.Text)),
                .LineCount = currentParagraph.Count,
                .Confidence = CSng(currentParagraph.Average(Function(b) If(b.Confidence, 0)))
            })
        End If

        Return sections
    End Function
End Class

Public Class DocumentSection
    Public Property Text As String
    Public Property LineCount As Integer
    Public Property Confidence As Single
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

//IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging

using IronOcr;

public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
    var ocr = new IronTesseract();
    var result = ocr.Read(imagePath);

    var sections = new List<DocumentSection>();

    foreach (var page in result.Pages)
    {
        foreach (var paragraph in page.Paragraphs)
        {
            sections.Add(new DocumentSection
            {
                Text = paragraph.Text,
                LineCount = paragraph.Lines.Length,
                // Coordinate access: exact pixel position on the page
                LocationX = paragraph.X,
                LocationY = paragraph.Y,
                Width = paragraph.Width,
                Height = paragraph.Height,
                Confidence = paragraph.Confidence
            });
        }
    }

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public int LocationX { get; set; }
    public int LocationY { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public double Confidence { get; set; }
}
//IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging

using IronOcr;

public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
    var ocr = new IronTesseract();
    var result = ocr.Read(imagePath);

    var sections = new List<DocumentSection>();

    foreach (var page in result.Pages)
    {
        foreach (var paragraph in page.Paragraphs)
        {
            sections.Add(new DocumentSection
            {
                Text = paragraph.Text,
                LineCount = paragraph.Lines.Length,
                // Coordinate access: exact pixel position on the page
                LocationX = paragraph.X,
                LocationY = paragraph.Y,
                Width = paragraph.Width,
                Height = paragraph.Height,
                Confidence = paragraph.Confidence
            });
        }
    }

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public int LocationX { get; set; }
    public int LocationY { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public double Confidence { get; set; }
}
Imports IronOcr

Public Function ExtractDocumentStructure(imagePath As String) As List(Of DocumentSection)
    Dim ocr As New IronTesseract()
    Dim result = ocr.Read(imagePath)

    Dim sections As New List(Of DocumentSection)()

    For Each page In result.Pages
        For Each paragraph In page.Paragraphs
            sections.Add(New DocumentSection With {
                .Text = paragraph.Text,
                .LineCount = paragraph.Lines.Length,
                .LocationX = paragraph.X,
                .LocationY = paragraph.Y,
                .Width = paragraph.Width,
                .Height = paragraph.Height,
                .Confidence = paragraph.Confidence
            })
        Next
    Next

    Return sections
End Function

Public Class DocumentSection
    Public Property Text As String
    Public Property LineCount As Integer
    Public Property LocationX As Integer
    Public Property LocationY As Integer
    Public Property Width As Integer
    Public Property Height As Integer
    Public Property Confidence As Double
End Class
$vbLabelText   $csharpLabel

단락, 줄 및 단어는 .X, .Y, .Width, .Height, 그리고 .Confidence 속성을 가진 타입된 컬렉션으로 직접 접근 가능합니다. 바운딩박스 정규화, 간격 임계값 휴리스틱, 블록 유형 필터링을 사용하지 않습니다. 해당 결과 읽기 가이드는 문자 수준 좌표 접근을 포함한 전체 OcrResult 계층 구조를 문서화합니다. 이 구조에 의존하는 송장 및 영수증 워크플로에 대해서는 송장 OCR 튜토리얼을 참조하세요.

속도 제한 배치 처리를 제약 없는 병렬 실행으로 대체

배치 이미지 처리에서는 Textract의 TPS 제한이 가장 눈에 띄는 운영 비용을 발생시킵니다. DetectDocumentText 동기 API는 속도 제한이 있습니다; 초당 5개 이상의 요청을 보내면 ProvisionedThroughputExceededException가 발생합니다. 올바른 배치 처리를 위해서는 SemaphoreSlim 제한, 백오프를 사용한 재시도 논리, 비행 중인 요청에 대한 신중한 계산이 필요합니다.IronOCR속도 제한이 없으며, 처리량은 사용 가능한 CPU 코어 수에 의해서만 제한됩니다.

AWS Textract 접근 방식:

// Batch processing requires SemaphoreSlim throttle + retry on rate limit exceeded

using Amazon.Textract;
using Amazon.Textract.Model;
using System.Collections.Concurrent;

public async Task<Dictionary<string, string>> ProcessImageBatchAsync(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var results = new ConcurrentDictionary<string, string>();
    var semaphore = new SemaphoreSlim(5); // 5 concurrent — Textract default TPS limit
    var tasks = new List<Task>();
    int completed = 0;

    foreach (var path in imagePaths)
    {
        await semaphore.WaitAsync();

        tasks.Add(Task.Run(async () =>
        {
            try
            {
                string text = null;
                int retryCount = 0;

                while (text == null && retryCount < 3)
                {
                    try
                    {
                        var bytes = await File.ReadAllBytesAsync(path);
                        var response = await _textractClient.DetectDocumentTextAsync(
                            new DetectDocumentTextRequest
                            {
                                Document = new Document { Bytes = new MemoryStream(bytes) }
                            });

                        text = string.Join("\n", response.Blocks
                            .Where(b => b.BlockType == BlockType.LINE)
                            .Select(b => b.Text));
                    }
                    catch (AmazonTextractException ex)
                        when (ex.ErrorCode == "ProvisionedThroughputExceededException")
                    {
                        // Exponential backoff on rate limit — blocks the entire thread
                        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
                        retryCount++;
                    }
                }

                results[path] = text ?? string.Empty;
                var done = Interlocked.Increment(ref completed);
                progress?.Report((done, imagePaths.Count));
            }
            finally
            {
                semaphore.Release();
            }
        }));
    }

    await Task.WhenAll(tasks);
    return results.ToDictionary(x => x.Key, x => x.Value);
}
// Batch processing requires SemaphoreSlim throttle + retry on rate limit exceeded

using Amazon.Textract;
using Amazon.Textract.Model;
using System.Collections.Concurrent;

public async Task<Dictionary<string, string>> ProcessImageBatchAsync(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var results = new ConcurrentDictionary<string, string>();
    var semaphore = new SemaphoreSlim(5); // 5 concurrent — Textract default TPS limit
    var tasks = new List<Task>();
    int completed = 0;

    foreach (var path in imagePaths)
    {
        await semaphore.WaitAsync();

        tasks.Add(Task.Run(async () =>
        {
            try
            {
                string text = null;
                int retryCount = 0;

                while (text == null && retryCount < 3)
                {
                    try
                    {
                        var bytes = await File.ReadAllBytesAsync(path);
                        var response = await _textractClient.DetectDocumentTextAsync(
                            new DetectDocumentTextRequest
                            {
                                Document = new Document { Bytes = new MemoryStream(bytes) }
                            });

                        text = string.Join("\n", response.Blocks
                            .Where(b => b.BlockType == BlockType.LINE)
                            .Select(b => b.Text));
                    }
                    catch (AmazonTextractException ex)
                        when (ex.ErrorCode == "ProvisionedThroughputExceededException")
                    {
                        // Exponential backoff on rate limit — blocks the entire thread
                        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
                        retryCount++;
                    }
                }

                results[path] = text ?? string.Empty;
                var done = Interlocked.Increment(ref completed);
                progress?.Report((done, imagePaths.Count));
            }
            finally
            {
                semaphore.Release();
            }
        }));
    }

    await Task.WhenAll(tasks);
    return results.ToDictionary(x => x.Key, x => x.Value);
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.Collections.Concurrent
Imports System.IO
Imports System.Threading

Public Async Function ProcessImageBatchAsync(
    imagePaths As IReadOnlyList(Of String),
    Optional progress As IProgress(Of (Completed As Integer, Total As Integer)) = Nothing) As Task(Of Dictionary(Of String, String))

    Dim results As New ConcurrentDictionary(Of String, String)()
    Dim semaphore As New SemaphoreSlim(5) ' 5 concurrent — Textract default TPS limit
    Dim tasks As New List(Of Task)()
    Dim completed As Integer = 0

    For Each path In imagePaths
        Await semaphore.WaitAsync()

        tasks.Add(Task.Run(Async Function()
            Try
                Dim text As String = Nothing
                Dim retryCount As Integer = 0

                While text Is Nothing AndAlso retryCount < 3
                    Try
                        Dim bytes = Await File.ReadAllBytesAsync(path)
                        Dim response = Await _textractClient.DetectDocumentTextAsync(
                            New DetectDocumentTextRequest With {
                                .Document = New Document With {.Bytes = New MemoryStream(bytes)}
                            })

                        text = String.Join(vbLf, response.Blocks _
                            .Where(Function(b) b.BlockType = BlockType.LINE) _
                            .Select(Function(b) b.Text))
                    Catch ex As AmazonTextractException When ex.ErrorCode = "ProvisionedThroughputExceededException"
                        ' Exponential backoff on rate limit — blocks the entire thread
                        Await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
                        retryCount += 1
                    End Try
                End While

                results(path) = If(text, String.Empty)
                Dim done = Interlocked.Increment(completed)
                If progress IsNot Nothing Then
                    progress.Report((done, imagePaths.Count))
                End If
            Finally
                semaphore.Release()
            End Try
        End Function))
    Next

    Await Task.WhenAll(tasks)
    Return results.ToDictionary(Function(x) x.Key, Function(x) x.Value)
End Function
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores

using IronOcr;
using System.Collections.Concurrent;

public Dictionary<string, string> ProcessImageBatch(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var ocr = new IronTesseract();
    var results = new ConcurrentDictionary<string, string>();
    int completed = 0;

    Parallel.ForEach(imagePaths, path =>
    {
        var result = ocr.Read(path);
        results[path] = result.Text;

        var done = Interlocked.Increment(ref completed);
        progress?.Report((done, imagePaths.Count));
    });

    return results.ToDictionary(x => x.Key, x => x.Value);
}
// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores

using IronOcr;
using System.Collections.Concurrent;

public Dictionary<string, string> ProcessImageBatch(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var ocr = new IronTesseract();
    var results = new ConcurrentDictionary<string, string>();
    int completed = 0;

    Parallel.ForEach(imagePaths, path =>
    {
        var result = ocr.Read(path);
        results[path] = result.Text;

        var done = Interlocked.Increment(ref completed);
        progress?.Report((done, imagePaths.Count));
    });

    return results.ToDictionary(x => x.Key, x => x.Value);
}
Imports IronOcr
Imports System.Collections.Concurrent
Imports System.Threading

Public Function ProcessImageBatch(
    imagePaths As IReadOnlyList(Of String),
    Optional progress As IProgress(Of (Completed As Integer, Total As Integer)) = Nothing) As Dictionary(Of String, String)

    Dim ocr As New IronTesseract()
    Dim results As New ConcurrentDictionary(Of String, String)()
    Dim completed As Integer = 0

    Parallel.ForEach(imagePaths, Sub(path)
                                     Dim result = ocr.Read(path)
                                     results(path) = result.Text

                                     Dim done = Interlocked.Increment(completed)
                                     If progress IsNot Nothing Then
                                         progress.Report((done, imagePaths.Count))
                                     End If
                                 End Sub)

    Return results.ToDictionary(Function(x) x.Key, Function(x) x.Value)
End Function
$vbLabelText   $csharpLabel

SemaphoreSlim, 재시도 루프, 지수적 백오프, 그리고 ProvisionedThroughputExceededException 캐치 블록은 모두 제거됩니다. IronTesseract는 스레드 안전하며, 스레드 간 공유되는 단일 인스턴스가 잠금 없이 전체 병렬 로드를 처리합니다. 8코어 컴퓨터에서는 별도의 설정 없이 8개의 문서를 동시에 처리할 수 있습니다. 32개 코어에서 32개를 동시에 사용합니다. 멀티스레딩 예제는 전체 패턴을 보여주고, 속도 최적화 가이드는 처리량 중심 배포를 위한 엔진 구성 튜닝을 다룹니다.

AnalyzeDocument의 형식 추출 기능을 영역 기반 CropRectangle OCR로 대체

Textract의 AnalyzeDocumentFORMSKEY_VALUE_SET 블록을 RelationshipType.VALUE 관계에 의해 연결하여 반환합니다. 폼 필드 키-값 쌍을 재구성하려면 EntityTypes.Contains("KEY")로 필터링하고, VALUE 관계 ID를 따르고, 자식 WORD 블록을 가져와 텍스트를 조립해야 합니다. 필드 위치가 알려진 고정 형식의 폼에서는 IronOCR의 CropRectangle가 관계 그래프 탐색 없이 각 필드를 직접 추출하며, 페이지당 비용이 없습니다.

AWS Textract 접근 방식:

// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    // $0.05/page for FORMS analysis
    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "FORMS" }
        });

    var allBlocks = response.Blocks;
    var fields = new Dictionary<string, string>();

    // Find KEY blocks only
    var keyBlocks = allBlocks.Where(b =>
        b.BlockType == BlockType.KEY_VALUE_SET &&
        b.EntityTypes?.Contains("KEY") == true);

    foreach (var keyBlock in keyBlocks)
    {
        // Assemble key text from CHILD WORD blocks
        var keyText = GetTextFromBlock(allBlocks, keyBlock);

        // Find associated VALUE block via VALUE relationship
        var valueBlockId = keyBlock.Relationships?
            .FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
            .Ids.FirstOrDefault();

        if (valueBlockId == null) continue;

        var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
        if (valueBlock == null) continue;

        var valueText = GetTextFromBlock(allBlocks, valueBlock);
        fields[keyText.TrimEnd(':')] = valueText;
    }

    return fields;
}

private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
    if (block.Relationships == null) return string.Empty;

    var childWordIds = block.Relationships
        .Where(r => r.Type == RelationshipType.CHILD)
        .SelectMany(r => r.Ids);

    return string.Join(" ", allBlocks
        .Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
        .Select(b => b.Text));
}
// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    // $0.05/page for FORMS analysis
    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "FORMS" }
        });

    var allBlocks = response.Blocks;
    var fields = new Dictionary<string, string>();

    // Find KEY blocks only
    var keyBlocks = allBlocks.Where(b =>
        b.BlockType == BlockType.KEY_VALUE_SET &&
        b.EntityTypes?.Contains("KEY") == true);

    foreach (var keyBlock in keyBlocks)
    {
        // Assemble key text from CHILD WORD blocks
        var keyText = GetTextFromBlock(allBlocks, keyBlock);

        // Find associated VALUE block via VALUE relationship
        var valueBlockId = keyBlock.Relationships?
            .FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
            .Ids.FirstOrDefault();

        if (valueBlockId == null) continue;

        var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
        if (valueBlock == null) continue;

        var valueText = GetTextFromBlock(allBlocks, valueBlock);
        fields[keyText.TrimEnd(':')] = valueText;
    }

    return fields;
}

private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
    if (block.Relationships == null) return string.Empty;

    var childWordIds = block.Relationships
        .Where(r => r.Type == RelationshipType.CHILD)
        .SelectMany(r => r.Ids);

    return string.Join(" ", allBlocks
        .Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
        .Select(b => b.Text));
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Threading.Tasks

Public Class InvoiceFieldExtractor
    Private _textractClient As AmazonTextractClient

    Public Async Function ExtractInvoiceFieldsAsync(imagePath As String) As Task(Of Dictionary(Of String, String))
        Dim bytes = File.ReadAllBytes(imagePath)

        ' $0.05/page for FORMS analysis
        Dim response = Await _textractClient.AnalyzeDocumentAsync(
            New AnalyzeDocumentRequest With {
                .Document = New Document With {.Bytes = New MemoryStream(bytes)},
                .FeatureTypes = New List(Of String) From {"FORMS"}
            })

        Dim allBlocks = response.Blocks
        Dim fields As New Dictionary(Of String, String)()

        ' Find KEY blocks only
        Dim keyBlocks = allBlocks.Where(Function(b) 
                                            Return b.BlockType = BlockType.KEY_VALUE_SET AndAlso
                                                   b.EntityTypes?.Contains("KEY") = True
                                        End Function)

        For Each keyBlock In keyBlocks
            ' Assemble key text from CHILD WORD blocks
            Dim keyText = GetTextFromBlock(allBlocks, keyBlock)

            ' Find associated VALUE block via VALUE relationship
            Dim valueBlockId = keyBlock.Relationships?.
                FirstOrDefault(Function(r) r.Type = RelationshipType.VALUE)?.
                Ids.FirstOrDefault()

            If valueBlockId Is Nothing Then Continue For

            Dim valueBlock = allBlocks.FirstOrDefault(Function(b) b.Id = valueBlockId)
            If valueBlock Is Nothing Then Continue For

            Dim valueText = GetTextFromBlock(allBlocks, valueBlock)
            fields(keyText.TrimEnd(":"c)) = valueText
        Next

        Return fields
    End Function

    Private Function GetTextFromBlock(allBlocks As IList(Of Block), block As Block) As String
        If block.Relationships Is Nothing Then Return String.Empty

        Dim childWordIds = block.Relationships.
            Where(Function(r) r.Type = RelationshipType.CHILD).
            SelectMany(Function(r) r.Ids)

        Return String.Join(" ", allBlocks.
            Where(Function(b) b.BlockType = BlockType.WORD AndAlso childWordIds.Contains(b.Id)).
            Select(Function(b) b.Text))
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known

using IronOcr;

// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
    new Dictionary<string, CropRectangle>
    {
        { "InvoiceNumber", new CropRectangle(450, 80,  300, 35) },
        { "InvoiceDate",   new CropRectangle(450, 120, 300, 35) },
        { "VendorName",    new CropRectangle(50,  160, 400, 35) },
        { "TotalAmount",   new CropRectangle(450, 680, 200, 35) },
        { "DueDate",       new CropRectangle(450, 720, 200, 35) }
    };

public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
    var ocr = new IronTesseract();
    var fields = new Dictionary<string, string>();

    foreach (var zone in InvoiceFieldZones)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, zone.Value); // load only the defined region

        var result = ocr.Read(input);
        fields[zone.Key] = result.Text.Trim();
    }

    return fields;
}
// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known

using IronOcr;

// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
    new Dictionary<string, CropRectangle>
    {
        { "InvoiceNumber", new CropRectangle(450, 80,  300, 35) },
        { "InvoiceDate",   new CropRectangle(450, 120, 300, 35) },
        { "VendorName",    new CropRectangle(50,  160, 400, 35) },
        { "TotalAmount",   new CropRectangle(450, 680, 200, 35) },
        { "DueDate",       new CropRectangle(450, 720, 200, 35) }
    };

public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
    var ocr = new IronTesseract();
    var fields = new Dictionary<string, string>();

    foreach (var zone in InvoiceFieldZones)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, zone.Value); // load only the defined region

        var result = ocr.Read(input);
        fields[zone.Key] = result.Text.Trim();
    }

    return fields;
}
Imports IronOcr

' CropRectangle extracts each field zone directly — no relationship graph
' Works for any fixed-format form where field positions are known

' Define the field zones for your form template once
Private Shared ReadOnly InvoiceFieldZones As New Dictionary(Of String, CropRectangle) From {
    {"InvoiceNumber", New CropRectangle(450, 80, 300, 35)},
    {"InvoiceDate", New CropRectangle(450, 120, 300, 35)},
    {"VendorName", New CropRectangle(50, 160, 400, 35)},
    {"TotalAmount", New CropRectangle(450, 680, 200, 35)},
    {"DueDate", New CropRectangle(450, 720, 200, 35)}
}

Public Function ExtractInvoiceFields(imagePath As String) As Dictionary(Of String, String)
    Dim ocr As New IronTesseract()
    Dim fields As New Dictionary(Of String, String)()

    For Each zone In InvoiceFieldZones
        Using input As New OcrInput()
            input.LoadImage(imagePath, zone.Value) ' load only the defined region

            Dim result = ocr.Read(input)
            fields(zone.Key) = result.Text.Trim()
        End Using
    Next

    Return fields
End Function
$vbLabelText   $csharpLabel

KEY_VALUE_SET 블록 필터링, RelationshipType.VALUE 탐색, 그리고 GetTextFromBlock 도우미가 제거됩니다. 각 필드는 해당 필드를 포함하는 픽셀 영역을 정확히 읽습니다. 그 이상도 이하도 아닙니다. 영역 기반 OCR 가이드CropRectangle 좌표 및 템플릿 측정치에서 영역 정의 방법을 다룹니다. 송장별 워크플로의 경우, 송장 OCR 블로그 게시물영수증 스캔 튜토리얼에서 전체 필드 추출 파이프라인을 확인할 수 있습니다.

AWS TextractAPI와IronOCR매핑 참조

AWS Textract IronOCR에 상응하는
AmazonTextractClient IronTesseract
AmazonS3Client 필요하지 않음
DetectDocumentTextRequest OcrInputinput.LoadImage()
DetectDocumentTextResponse OcrResult
AnalyzeDocumentRequest (TABLES/FORMS) OcrInput와 선택적 CropRectangle
StartDocumentTextDetectionRequest OcrInputinput.LoadPdf() — 동기적, 작업 시작 없음
GetDocumentTextDetectionRequest 해당 사항 없음 - 결과는 즉시 확인됩니다.
Document.Bytes input.LoadImage(byteArray) 또는 input.LoadImage(stream)
S3Object (문서 준비) 파일 경로 또는 스트림 - 스테이징 필요 없음
Block (BlockType.LINE) result.Lines (타입된 컬렉션)
Block (BlockType.WORD) result.Words (타입된 컬렉션)
Block (BlockType.TABLE) result.Words 좌표 근접성 그룹화
Block (BlockType.KEY_VALUE_SET) CropRectangle 필드별 영역 추출
Block.Confidence word.Confidence / result.Confidence
Block.Geometry.BoundingBox word.X, word.Y, word.Width, word.Height
RelationshipType.CHILD 탐색 형식화된 결과 객체에 대한 직접 속성 접근
JobStatus.IN_PROGRESS 해당 사항 없음 — 비동기 상태 없음
JobStatus.SUCCEEDED 해당 없음 — 동기식 반환
response.NextToken (페이지 매김) 해당 사항 없음 — 결과가 페이지별로 표시되지 않습니다.
ProvisionedThroughputExceededException 해당 사항 없음 - TPS 제한 없음
AccessDeniedException 해당 사항 없음 — 자격증명 체계가 없음
input.LoadImageFrames() 멀티프레임 TIFF 직접 지원 (Textract와 동일한 기능은 없음)
result.SaveAsSearchablePdf() 검색 가능한 PDF 출력 (Textract와 같은 기능은 없음)

일반적인 마이그레이션 문제와 해결책

문제 1: 새 배포 환경에서 자격 증명 구성 오류

AWS Textract: AmazonTextractClient 생성자는 자격 증명을 체인에서 해결합니다: 환경 변수, ~/.aws/credentials, EC2 인스턴스 프로필, ECS 작업 역할. 새로운 Docker 컨테이너 또는 CI 환경에서, 이러한 것이 구성되지 않은 경우 클라이언트는 오류 없이 구성되지만 모든 API 호출은 AmazonClientException: No credentials specified를 던집니다. 해당 오류는 런타임 전까지는 보이지 않습니다.

해결책: 자격 증명 체인을 완전히 제거합니다. 구성이 더 간편하고 IAM 권한 관리가 필요 없는 환경 변수를 사용하여IronOCR라이선스 키를 설정하세요.

// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
    throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");

IronOcr.License.LicenseKey = key;
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
    throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");

IronOcr.License.LicenseKey = key;
Imports System

' Single environment variable — no IAM, no rotation, no chain resolution
Dim key As String = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
If String.IsNullOrEmpty(key) Then
    Throw New InvalidOperationException("IRONOCR_LICENSE environment variable is not set")
End If

IronOcr.License.LicenseKey = key
$vbLabelText   $csharpLabel

문제 2: 코드베이스 전체에 걸친 비동기 호출 위치

AWS Textract: 전체 SDK는 비동기 전용 (DetectDocumentTextAsync, StartDocumentTextDetectionAsync, GetDocumentTextDetectionAsync) 이므로 모든 Textract 호출 사이트는 호출 스택을 통해 async Task<t>를 전달합니다. IronOCR의 동기식 API로 마이그레이션하려면 각 사이트에서 결정을 내려야 합니다.

해결책: 백그라운드 처리 서비스 및 컨트롤러 작업의 경우, IronOCR의 동기 호출은 백그라운드 스레드에서 안전하게 실행됩니다. 기존 호출 체인이 비동기로 남아 있어야 할 때에만 Task.Run로 래핑하십시오:

// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
    using var ms = new MemoryStream();
    await file.CopyToAsync(ms);

    // Offload synchronous OCR to thread pool — keeps controller non-blocking
    var text = await Task.Run(() =>
    {
        var ocr = new IronTesseract();
        using var input = new OcrInput();
        input.LoadImage(ms.ToArray());
        return ocr.Read(input).Text;
    });

    return Ok(text);
}
// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
    using var ms = new MemoryStream();
    await file.CopyToAsync(ms);

    // Offload synchronous OCR to thread pool — keeps controller non-blocking
    var text = await Task.Run(() =>
    {
        var ocr = new IronTesseract();
        using var input = new OcrInput();
        input.LoadImage(ms.ToArray());
        return ocr.Read(input).Text;
    });

    return Ok(text);
}
Imports System.IO
Imports System.Threading.Tasks
Imports Microsoft.AspNetCore.Mvc

Public Class YourController
    Inherits Controller

    ' If the call site must remain async (e.g., an ASP.NET controller action):
    Public Async Function ProcessUpload(file As IFormFile) As Task(Of IActionResult)
        Using ms As New MemoryStream()
            Await file.CopyToAsync(ms)

            ' Offload synchronous OCR to thread pool — keeps controller non-blocking
            Dim text = Await Task.Run(Function()
                                          Dim ocr = New IronTesseract()
                                          Using input As New OcrInput()
                                              input.LoadImage(ms.ToArray())
                                              Return ocr.Read(input).Text
                                          End Using
                                      End Function)

            Return Ok(text)
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

비 UI 스레드에서 이미 실행 중인 배치 프로세서와 백그라운드 서비스의 경우, ocr.Read()를 동기적으로 호출하십시오 — 그런 컨텍스트에서는 Task.Run가 이익 없이 오버헤드를 추가합니다. 비동기 OCR 가이드에서는 권장 패턴을 다룹니다.

문제 3: S3 버킷 정리 로직이 코드 곳곳에 흩어져 있음

AWS Textract: Textract 처리를 위해 S3에 업로드되는 모든 코드는 작업 완료 후 S3에서 삭제되어야 합니다. 이 정리는 종종 finally 블록에 있으며, 오류 경로에서는 생략되어 저장 비용을 증가시키는 고아 객체를 유발합니다. 마이그레이션 과정에서 S3 정리 코드는 OCR과 개념적으로 관련이 없기 때문에 간과하기 쉽습니다.

해결책: S3 업로드 및 정리 패턴 전체에 대해IronOCR해당하는 기능이 없습니다. 모든 PutObjectAsync, DeleteObjectAsync 및 S3 관련 try/finally 블록을 전부 삭제하십시오.IronOCR로컬 경로 또는 스트림에서 직접 읽습니다.

// Before: upload → job → poll → extract → cleanup (50+ lines)
// After: two lines, no cleanup required

using var input = new OcrInput();
input.LoadPdf(localPdfPath);
var text = new IronTesseract().Read(input).Text;
// Before: upload → job → poll → extract → cleanup (50+ lines)
// After: two lines, no cleanup required

using var input = new OcrInput();
input.LoadPdf(localPdfPath);
var text = new IronTesseract().Read(input).Text;
Imports IronOcr

Using input As New OcrInput()
    input.LoadPdf(localPdfPath)
    Dim text As String = New IronTesseract().Read(input).Text
End Using
$vbLabelText   $csharpLabel

마이그레이션 후 S3 스테이징 버킷을 사용 중지하세요. PDF 입력 가이드스트림 입력 가이드는 S3 스테이징 문서 액세스를 대체하는 모든 입력 패턴을 다룹니다.

문제 4: 블록 그래프 순회 코드에는 직접적인 동등물이 없다

AWS Textract: Block.Relationships를 탐색하여 RelationshipType.CHILD ID 배열에서 테이블 셀, 폼 필드, 또는 단락 그룹을 재구성하는 코드는 마이그레이션 시간 소모가 가장 큰 코드입니다.IronOCR관계 그래프 대신 형식화된 컬렉션을 반환하므로 일대일 대체는 불가능합니다.

해결 방법: 각 순회 패턴을 적절한 형식의 속성으로 대체합니다. 관계 ID 조회를 통해 속성에 직접 접근할 수 있습니다.

// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
    .Where(b => b.BlockType == BlockType.WORD &&
                childIds.Contains(b.Id))
    .Select(b => b.Text));

// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
    foreach (var para in page.Paragraphs)
        Console.WriteLine(para.Text); // already assembled
// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
    .Where(b => b.BlockType == BlockType.WORD &&
                childIds.Contains(b.Id))
    .Select(b => b.Text));

// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
    foreach (var para in page.Paragraphs)
        Console.WriteLine(para.Text); // already assembled
' Before: filter by BlockType + traverse relationship IDs
Dim paragraphText As String = String.Join(" ", allBlocks _
    .Where(Function(b) b.BlockType = BlockType.WORD AndAlso _
                childIds.Contains(b.Id)) _
    .Select(Function(b) b.Text))

' After: direct paragraph text (no filtering, no relationship lookup)
For Each page In result.Pages
    For Each para In page.Paragraphs
        Console.WriteLine(para.Text) ' already assembled
    Next
Next
$vbLabelText   $csharpLabel

BlockType.CELLColumnIndex 속성에 의존한 테이블 재구성의 경우, result.Words에서 단어 좌표 그룹화를 사용하십시오. 표 읽기 안내서는 좌표 기반 접근 방식을 다룹니다.

문제 5: ProvisionedThroughputExceededException 예외 처리 블록

AWS Textract: 견고한 배치 처리 코드는 AmazonTextractExceptionErrorCode == "ProvisionedThroughputExceededException"와 함께 잡고 백오프를 사용한 재시도를 구현합니다. 이 오류 코드는 Textract 고유의 개념으로IronOCR상응하는 개념이 없습니다.

해결책: 모든 ProvisionedThroughputExceededException 캐치 블록을 삭제하십시오.IronOCR에는 TPS 제한이 없습니다. 전체 Parallel.ForEach로 스로틀된 배치 패턴을 교체하십시오:

// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
    var result = new IronTesseract().Read(path);
    results[path] = result.Text;
});
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
    var result = new IronTesseract().Read(path);
    results[path] = result.Text;
});
Imports System.Threading.Tasks

Parallel.ForEach(imagePaths, Sub(path)
    Dim result = New IronTesseract().Read(path)
    results(path) = result.Text
End Sub)
$vbLabelText   $csharpLabel

문제 6: 클라이언트 생성자에 지역 구성이 포함됨

AWS Textract: new AmazonTextractClient(Amazon.RegionEndpoint.USEast1)가 지역을 하드 코딩합니다. 다중 지역 배포에는 여러 클라이언트 인스턴스 또는 동적 지역 선택이 필요합니다. Textract가 새로운 지역에 대한 지원을 추가할 경우 코드를 업데이트해야 합니다.

해결책:IronOCR에는 지역 개념이 없습니다. 모든 Amazon.RegionEndpoint 참조를 제거하십시오. IronTesseract 생성자는 네트워크 구성이 필요하지 않습니다 - 처리는 로컬에서 이루어집니다. 각 리전에서 자체 컴퓨팅을 실행하는 AWS 인프라의 다중 리전 배포의 경우, 각IronOCR인스턴스는 리전 간 호출 없이 해당 컴퓨팅 경계 내에서 로컬로 문서를 처리합니다.

AWS Textract마이그레이션 체크리스트

사전 마이그레이션

코드베이스 내 Textract 및 S3 SDK 사용 내역을 모두 감사하십시오.

grep -rn "Amazon.Textract\|AmazonTextractClient\|DetectDocumentText" --include="*.cs" .
grep -rn "StartDocumentTextDetection\|GetDocumentTextDetection\|JobStatus" --include="*.cs" .
grep -rn "AmazonS3Client\|PutObjectRequest\|DeleteObjectAsync" --include="*.cs" .
grep -rn "BlockType\|RelationshipType\|KEY_VALUE_SET" --include="*.cs" .
grep -rn "ProvisionedThroughputExceededException\|AmazonTextractException" --include="*.cs" .
grep -rn "AWSSDK\|Amazon.RegionEndpoint" --include="*.csproj" .
grep -rn "Amazon.Textract\|AmazonTextractClient\|DetectDocumentText" --include="*.cs" .
grep -rn "StartDocumentTextDetection\|GetDocumentTextDetection\|JobStatus" --include="*.cs" .
grep -rn "AmazonS3Client\|PutObjectRequest\|DeleteObjectAsync" --include="*.cs" .
grep -rn "BlockType\|RelationshipType\|KEY_VALUE_SET" --include="*.cs" .
grep -rn "ProvisionedThroughputExceededException\|AmazonTextractException" --include="*.cs" .
grep -rn "AWSSDK\|Amazon.RegionEndpoint" --include="*.csproj" .
SHELL

교체 코드를 작성하기 전에 다음 사항을 확인하십시오.

  • AmazonTextractClient가 포함된 모든 파일을 세십시오 — 각 파일은 대체 대상입니다
  • 모든 AnalyzeDocument 호출 지점을 나열하십시오 — TABLES, FORMS, 혹은 둘 다 사용되었는지 확인하십시오
  • 모든 비동기 폴링 루프를 식별하십시오 (do { await Task.Delay } while JobStatus == IN_PROGRESS)
  • 모든 S3 정리 finally 블록을 찾으십시오 — 이것들은 전체 삭제되며, 대체되지 않습니다
  • 모든 ProvisionedThroughputExceededException 캐치 블록을 세십시오 — 삭제되며, 대체되지 않습니다
  • 모든 BlockType.TABLE, BlockType.CELL, BlockType.KEY_VALUE_SET 탐색 로직을 주목하십시오 — 각각 구조화된 출력 대체가 필요합니다

코드 마이그레이션

  1. 모든 .csproj 파일에서 AWSSDK.Textract, AWSSDK.S3AWSSDK.Core를 제거하십시오
  2. OCR을 수행하는 각 프로젝트에서 dotnet add package IronOcr을 실행하십시오
  3. 응용 프로그램 시작 시 (Program.cs 또는 동등) 한 번 IronOcr.License.LicenseKey = ...를 추가하십시오
  4. 모든 using Amazon.Textract;, using Amazon.Textract.Model;, using Amazon.S3;, using Amazon.Runtime; 문을 제거하십시오
  5. 이전에 Textract 네임스페이스를 임포트한 각 파일에 using IronOcr;를 추가하십시오
  6. new IronTesseract()AmazonTextractClient 생성자 호출을 교체하십시오
  7. PutObjectAsync / DeleteObjectAsync 호출과 모든 AmazonS3Client 인스턴스를 직접 파일 경로 또는 스트림 읽기로 교체하십시오
  8. 모든 DetectDocumentTextAsync 호출을 교체하십시오: var text = ocr.Read(path).Text
  9. 모든 StartDocumentTextDetectionAsync + 폴링 루프를 input.LoadPdf(path) + ocr.Read(input)로 교체하십시오
  10. 모든 다프레임 TIFF 파이프라인(TIFF → PDF → S3 → 비동기)을 input.LoadImageFrames(tiffPath)로 교체하십시오
  11. result.Lines / result.WordsBlockType.LINE / BlockType.WORD 필터 체인을 교체하십시오
  12. CropRectangle 영역 추출로 BlockType.KEY_VALUE_SET 관계 탐색을 교체하십시오
  13. 모든 ProvisionedThroughputExceededException 캐치 블록 및 SemaphoreSlim 스로틀을 삭제하십시오
  14. 공유 IronTesseract 인스턴스를 사용하여 Parallel.ForEach로 스로틀된 배치 패턴을 교체하십시오
  15. 배포 매니페스트 및 CI 파이프라인에서 모든 AWS 자격 증명 환경 변수 구성을 제거합니다.
  16. 모든 처리 코드의 마이그레이션 및 검증이 완료되면 S3 스테이징 버킷을 해제합니다.

마이그레이션 이후

  • 모든 AWS 환경 변수가 제거된 상태에서 IRONOCR_LICENSE만 설정된 애플리케이션이 깨끗하게 시작되는지 확인하십시오
  • Textract로 이전에 처리한 동일한 샘플 이미지에 대해 OCR을 실행하고 추출된 텍스트의 정확도를 비교합니다.
  • S3 또는 비동기 폴링 없이 여러 페이지로 구성된 PDF 파일을 직접 처리하고 모든 페이지가 추출되었는지 확인합니다.
  • 다중 프레임 TIFF 파일을 처리하고 페이지 수가 Textract에서 동일한 문서에 대해 출력한 결과와 일치하는지 확인합니다.
  • 50개 이상의 이미지로 배치 처리를 테스트하고 ProvisionedThroughputExceededException에 상응하는 것이 발생하지 않는지 확인하십시오
  • 외부 인터넷 접속이 차단된 Docker 컨테이너에서 애플리케이션을 실행하고 OCR이 성공적으로 완료되는지 확인합니다.
  • 검색 가능한 PDF 출력물이 Adobe Reader에서 올바르게 열리고 전체 텍스트 검색이 가능한지 확인합니다.
  • 배포 환경에서 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY를 제거해도 아무 문제가 발생하지 않음을 확인하십시오
  • Textract에서 생성된 알려진 출력 결과를 기준으로 양식 또는 송장 추출 워크플로를 테스트하여 필드 정확성을 검증합니다.
  • 대표적인 문서 세트에 대한 처리 지연 시간을 측정하고 최소 5초의 폴링 대기 시간이 제거되었는지 확인합니다.

IronOCR로 마이그레이션할 때의 주요 이점

인프라를 단일 NuGet 패키지로 통합했습니다. 세 개의 AWS SDK 패키지, 수명 주기 정책이 적용된 S3 버킷, 모든 배포 환경에 걸쳐 사용되는 IAM 역할, 그리고 AWS 자격 증명 순환 절차가 하나의 NuGet 패키지로 대체되었습니다. .csproj 변경은 dotnet remove package AWSSDK.Textract에 이어 dotnet add package IronOcr가 따릅니다. AWS 자격 증명 관리의 모든 후속 결과(보안 감사, 자격 증명 교체 일정, 환경 변수 관리, Docker 비밀 구성 등)가 함께 사라집니다.

예측 가능한 총 소유 비용. 페이지당 청구 모델은 문서량에 따라 무한히 증가하는 가변 비용을 발생시킵니다.IronOCR로 마이그레이션한 후에는 처리량에 관계없이 추가 페이지당 비용이 무료입니다. Textract AnalyzeDocument 요율로 매달 200,000 페이지를 처리하는 팀은 OCR에만 연간 $36,000를 소비합니다.IronOCR Enterprise 라이선스 가격은 2,999달러이며, 청구서 발행을 31일 이내에 피할 수 있으므로 투자 비용을 회수할 수 있습니다. 자세한 등급별 정보 및 SaaS 재배포 조건은 IronOCR 라이선스 페이지를 참조하십시오.

동기 처리는 5단계 비동기 처리의 복잡성을 제거합니다. S3 업로드, 작업 시작, 폴링 루프, 페이지별 결과 수집 및 정리의 finally 블록은 Textract PDF 파이프라인에서 발생하는 5가지 독립적인 오류 발생 가능성을 나타냅니다. 마이그레이션 후 PDF는 두 줄만 읽어들입니다. 오류 처리는 ocr.Read() 호출 주위의 단일 try/catch로 줄어듭니다. 폴링 타임아웃 논리, do/while JobStatus == IN_PROGRESS 루프, nextToken 페이지 매김 누적기 — 이러한 개념은IronOCR코드베이스에서는 존재하지 않습니다. 유지보수 부담은 제거된 코드의 양에 비례하여 감소합니다.

완벽한 배포 유연성. Textract는 모든 OCR 호출 시 인터넷 연결이 필요합니다.IronOCR설치 시 NuGet 패키지를 다운로드하기 위해서만 인터넷에 접속합니다. 이후에는 외부 접근이 차단된 네트워크, 외부 접근 규칙이 없는 Docker 컨테이너, 온프레미스 서버, Textract 접근 권한이 없는 AWS EC2 인스턴스에서 작동합니다. Windows Server에서 프로덕션 환경으로 실행되는 것과 동일한 바이너리가 Linux 컨테이너에서도 실행됩니다. Docker 배포 가이드Linux 배포 가이드는 이전에 Textract 사용이 차단되었던 컨테이너 환경에 대한 특정 구성 방법을 다룹니다.

데이터 주권은 설정이 아닌 아키텍처를 통해 달성됩니다.IronOCR비활성화할 수 있는 네트워크 전송 모드가 없으며, 로컬 처리 모드만 있습니다.IronOCR통해 처리된 문서는 호스트 시스템을 벗어나지 않습니다. 의료 관련 개인 건강 정보(PHI)를 처리하는 의료 애플리케이션, 통제되지 않은 정보(CUI)를 처리하는 국방 애플리케이션, 결제 카드 이미지를 처리하는 금융 애플리케이션의 경우, 이러한 방식은 사업 제휴 계약(BAA) 협상, 지역 데이터 상주 설정, 아마존의 데이터 처리 정책 감사 등이 필요 없는 규정 준수 환경을 제공합니다. 규정 준수 범위는 이미 여러분이 통제하고 있는 인프라 경계입니다. IronOCR 제품 페이지는 규정 준수 검토를 수행하는 팀을 위해 전체 기능 요약 및 배포 문서를 제공합니다.

문서 품질 관리를 위한 구성 가능한 전처리 기능. Textract의 내부 전처리 기능은 접근하거나 구성할 수 없습니다. 스캔한 문서의 결과물이 좋지 않을 경우, 유일한 해결책은 결과물을 그대로 받아들이거나 다시 스캔하는 것입니다. IronOCR는 전체 전처리 파이프라인을 노출합니다: Deskew(), DeNoise(), Contrast(), Binarize(), Sharpen(), Scale(), Dilate(), Erode(), DeepCleanBackgroundNoise(). 스캔 결과가 만족스럽지 못해 Textract에서 문서를 다른 곳으로 옮긴 팀은 회전, 노이즈, 낮은 대비 또는 해상도 부족과 같은 특정 결함에 맞는 필터를 사용하여 근본 원인을 직접 해결할 수 있습니다. 전처리 기능 페이지이미지 품질 보정 가이드 에는 사용 가능한 필터와 각 필터의 ​​적절한 사용 사례가 설명되어 있습니다.

참고해 주세요AWS Textract, PDFSharp, Tesseract, 및 iText는 각자의 등록 상표입니다. 이 사이트는 Amazon Web Services, Google, empira Software GmbH, 또는 iText Group과 관련이 없으며, 모든 제품 이름, 로고 및 브랜드는 각자의 소유자에게 귀속합니다. 비교는 정보 제공 목적으로만 사용되며, 작성 시점에 공개적으로 이용 가능한 정보를 반영합니다.

자주 묻는 질문

Amazon Textract에서 IronOCR로 마이그레이션해야 하는 이유는 무엇인가요?

일반적인 동인으로는 COM 상호 운용의 복잡성 제거, 파일 기반 라이선스 관리 대체, 페이지당 과금 방지, Docker/컨테이너 배포 활성화, 표준 .NET 툴링과 통합되는 NuGet 네이티브 워크플로 채택 등이 있습니다.

Amazon Textract에서 IronOCR로 마이그레이션할 때 주요 코드 변경 사항은 무엇인가요?

Amazon Textract 초기화 시퀀스를 IronTesseract 인스턴스화로 대체하고, COM 수명 주기 관리(명시적 생성/로드/폐쇄 패턴)를 제거하고, 결과 속성 이름을 업데이트합니다. 그 결과 상용구 줄이 훨씬 줄어듭니다.

마이그레이션을 시작하려면 IronOCR을 어떻게 설치하나요?

패키지 관리자 콘솔에서 '설치-패키지 IronOcr'을 실행하거나 CLI에서 '닷넷 추가 패키지 IronOcr'을 실행하세요. 언어 팩은 별도의 패키지입니다: 예를 들어 프랑스어의 경우 '닷넷 추가 패키지 IronOcr.Languages.French'를 실행합니다.

IronOCR은 표준 비즈니스 문서에 대해 Amazon Textract의 OCR 정확도와 일치하나요?

IronOCR은 송장, 계약서, 영수증, 타이핑된 양식 등 표준 비즈니스 콘텐츠에 대해 높은 정확도를 달성합니다. 이미지 전처리 필터(데스큐, 노이즈 제거, 대비 향상)는 품질이 저하된 입력에 대한 인식률을 더욱 향상시킵니다.

IronOCR은 Amazon Textract가 별도로 설치하는 언어 데이터를 어떻게 처리하나요?

IronOCR의 언어 데이터는 NuGet 패키지로 배포됩니다. '닷넷 추가 패키지 IronOcr.Languages.German'은 독일어 지원을 설치합니다. 수동 파일 배치나 디렉터리 경로는 필요하지 않습니다.

Amazon Textract에서 IronOCR로 마이그레이션하려면 배포 인프라를 변경해야 하나요?

IronOCR은 Amazon Textract보다 인프라 변경이 덜 필요합니다. SDK 바이너리 경로, 라이선스 파일 배치 또는 라이선스 서버 구성이 필요하지 않습니다. NuGet 패키지에는 완전한 OCR 엔진이 포함되어 있으며 라이선스 키는 애플리케이션 코드에 설정된 문자열입니다.

마이그레이션 후 IronOCR 라이선싱은 어떻게 구성하나요?

애플리케이션 시작 코드에 IronOcr.License.LicenseKey = "YOUR-KEY"를 할당하세요. Docker 또는 Kubernetes에서 키를 환경 변수로 저장하고 시작 시 키를 읽습니다. 트래픽을 수락하기 전에 License.IsValidLicense를 사용하여 유효성을 검사하세요.

IronOCR은 Amazon Textract와 동일한 방식으로 PDF를 처리할 수 있나요?

예. IronOCR은 원본 및 스캔한 PDF를 모두 읽습니다. IronTesseract를 인스턴스화하고, 입력이 PDF 경로 또는 OcrPdfInput인 경우 ocr.Read(input)를 호출하고, OcrResult 페이지를 반복하면 됩니다. 별도의 PDF 렌더링 파이프라인이 필요하지 않습니다.

IronOCR은 대량 처리에서 스레딩을 어떻게 처리하나요?

IronTesseract는 스레드별로 인스턴스화해도 안전합니다. Parallel.ForEach 또는 Task 풀에서 스레드당 하나의 인스턴스를 스핀업하고, OCR을 동시에 실행하고, 완료되면 각 인스턴스를 폐기하면 됩니다. 전역 상태나 잠금이 필요하지 않습니다.

IronOCR은 텍스트 추출 후 어떤 출력 형식을 지원하나요?

IronOCR은 텍스트, 단어 좌표, 신뢰도 점수, 페이지 구조를 포함한 구조화된 결과를 반환합니다. 내보내기 옵션에는 일반 텍스트, 검색 가능한 PDF, 다운스트림 처리를 위한 구조화된 결과 개체가 포함됩니다.

워크로드 확장에 대해 Amazon Textract보다 IronOCR 가격이 더 예측 가능한가요?

IronOCR은 페이지당 또는 볼륨당 요금이 없는 정액제 영구 라이선스를 사용합니다. 10,000페이지를 처리하든 1,000만 페이지를 처리하든 라이선스 비용은 일정하게 유지됩니다. 볼륨 및 팀 라이선스 옵션은 IronOCR 가격 페이지에서 확인할 수 있습니다.

Amazon Textract에서 IronOCR로 마이그레이션한 후 기존 테스트는 어떻게 되나요?

추출된 텍스트 콘텐츠에 대해 어설트하는 테스트는 마이그레이션 후에도 계속 통과해야 합니다. API 호출 패턴 또는 COM 개체 수명 주기의 유효성을 검사하는 테스트는 IronOCR의 더 간단한 초기화 및 결과 모델을 반영하도록 업데이트해야 합니다.

칸나오팟 우돈판트
소프트웨어 엔지니어
카나팟은 소프트웨어 엔지니어가 되기 전 일본 홋카이도 대학교에서 환경 자원학 박사 학위를 취득했습니다. 학위 과정 중에는 생물생산공학과 소속 차량 로봇 연구실에서 활동하기도 했습니다. 2022년에는 C# 기술을 활용하여 Iron Software의 엔지니어링 팀에 합류했고, 현재 IronPDF 개발에 집중하고 있습니다. 카나팟은 IronPDF에 사용되는 대부분의 코드를 직접 작성하는 개발자로부터 배울 수 있다는 점에 만족하며, 동료들과의 소통을 통해 배우는 것 외에도 Iron Software에서 일하는 즐거움을 누리고 있습니다. 코딩이나 문서 작업을 하지 않을 때는 주로 PS5로 게임을 하거나 The Last of Us를 다시 시청하는 것을 즐깁니다.

아이언 서포트 팀

저희는 주 5일, 24시간 온라인으로 운영합니다.
채팅
이메일
전화해