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

NET Maui에서 OCR을 수행하는 방법

이 가이드는 .NET 개발자가 LEADTOOLS OCR에서 IronOCR 로 완전히 마이그레이션하는 과정을 안내합니다. 이 문서에서는 NuGet 패키지 교체부터 전체 코드 마이그레이션까지 모든 단계를 다루며, LEADTOOLS의 초기화 절차, 다중 네임스페이스 구조 및 파일 기반 라이선스 배포 모델의 영향을 가장 많이 받는 패턴에 대한 마이그레이션 전후 예시를 제공합니다.

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

LEADTOOLS OCR은 1990년대로 거슬러 올라가는 역사를 가진 Enterprise 이미지 처리 플랫폼에 포함되어 제공되며, API 또한 이러한 계보를 반영합니다. 최소 작동 구성에는 4개의 NuGet 패키지, 4개의 네임스페이스, 알려진 경로에 배포된 2개의 물리적 라이선스 파일, 엔진 전에 초기화되는 코덱 계층, 3가지 옵션 중에서 선택하는 엔진 유형, 그리고 런타임 바이너리를 메모리에 로드하는 차단 시작 호출이 필요합니다. 이 모든 과정이 문자 하나가 인식되기 전에 실행됩니다.IronOCR로 마이그레이션하는 팀은 해당 계층 전체를 제거합니다. 패키지 하나, 네임스페이스 하나, 라이선스 키를 설정하는 단 한 줄만 있으면 됩니다.

컨테이너에서 파일 기반의 라이선스 배포 중단. LEADTOOLS는 두 개의 물리적 파일, LEADTOOLS.LICLEADTOOLS.LIC.KEY,을 필요로 하며, 애플리케이션을 실행하는 모든 기계에서 특정 경로에서 접근 가능해야 합니다. Docker 컨테이너에서 이러한 파일은 이미지에 포함시키거나(레이어 기록에 노출) 런타임에 마운트해야 합니다(모든 오케스트레이션 환경에서 볼륨 조정 필요). Azure Functions와 AWS Lambda는 별도의 해결 방법 없이는 라이선스 파일을 배포할 수 있는 메커니즘을 제공하지 않습니다. CI/CD 파이프라인은 시작 호출에서 사용하는 정확한 경로에 파일이 있어야 합니다. 그렇지 않으면 애플리케이션이 단 하나의 문서도 처리하기 전에 오류가 발생합니다.IronOCR두 파일 모두를 환경 변수, Kubernetes 시크릿 또는 Azure Key Vault 참조에 맞는 문자열로 바꿉니다.

하나의 작업을 위한 네 개의 패키지. 작동하는 리드툴즈 OCR 프로젝트는 최소한 Leadtools, Leadtools.Ocr, Leadtools.CodecsLeadtools.Forms.DocumentWriters가 필요합니다. 비밀번호로 보호된 PDF 지원은 구매한 번들에 포함되지 않을 수 있는 추가 Leadtools.Pdf 모듈을 필요로 합니다. 각 패키지는 반드시 존재해야 하고, 호환되어야 하며, 동일한 버전으로 제공되어야 합니다.IronOCR하나의 NuGet 패키지로 제공됩니다. PDF 입력, 검색 가능한 PDF 출력, 전처리, 바코드 판독 등 모든 기능이 포함되어 있습니다.

엔진 수명 주기는 유지보수 표면을 생성합니다. LEADTOOLS는 OCR 엔진을 IDisposable 서비스 클래스에 감싸고 있습니다. 이는 관용적 .NET 이유가 아닌, 애플리케이션이 오류를 생성하지 않도록 Dispose() 이전에 Shutdown() 호출을 해야 하기 때문입니다. LEADTOOLS 배치 프로세서의 생산 구현은 일반적으로 누적되는 RasterImage 인스턴스를 보상하기 위해 문서 청크 사이에 GC.Collect() / GC.WaitForPendingFinalizers() 호출을 포함합니다. IronOCR는 표준 using 블록을 사용합니다. OcrInput는 처분이 필요한 유일한 객체입니다.

번들 관련 혼란이 실제 운영 환경에서 발생합니다. LEADTOOLS는 가격 정보를 공개적으로 게시하지 않습니다. OCR 기능을 포함하는 문서 이미지 처리 SDK는 개발자 1인당 연간 약 3,000달러에서 8,000달러의 비용이 드는 것으로 추정됩니다. PDF 모듈 없이 OCR 모듈을 구매한 팀은 생산에서 비밀번호로 보호된 문서에 대해 RasterSupport.IsLocked()가 true를 반환할 때 간격을 발견합니다. OmniPage 엔진 정확도 등급을 사용하려면 LEADTOOLS 구매 외에 별도의 Kofax 라이선스 계약이 필요합니다.IronOCR각 등급별로 모든 기능에 대한 라이선스를 제공합니다. 별도의 공급업체와의 관계도 없고, 암호화된 문서를 위한 별도의 모듈도 없습니다.

초기화 비용이 콜드 스타트에 영향을 미칩니다. engine.Startup()는 런타임 파일을 메모리에 로드하는 블로킹 호출입니다. 콜드 스타트 ​​지연 시간이 중요한 서버리스 환경(Azure Functions, AWS Lambda 등)에서 인식 작업이 시작되기 전에 500~2000ms 동안 지속되는 초기화 과정은 구조적인 문제입니다.IronOCR지연 초기화를 사용합니다. IronTesseract 인스턴스는 최초 사용 시 초기화되며, 동일한 프로세스 내 후속 호출에는 초기화 비용이 전혀 발생하지 않습니다.

근본적인 문제

LEADTOOLS를 사용하려면 네 개의 네임스페이스, 네 개의 NuGet 패키지, 그리고 문자를 인식하기 전에 반드시 거쳐야 하는 시작 절차가 필요합니다.

// LEADTOOLS: Four namespaces, four packages, six steps before recognition
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath)); // Step 1: two files on disk
var codecs = new RasterCodecs();                              // Step 2: codec layer
var engine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD); // Step 3: engine factory
engine.Startup(codecs, null, null, runtimePath);              // Step 4: blocking startup
// ... still need to create document, add page, call Recognize(), extract text
// LEADTOOLS: Four namespaces, four packages, six steps before recognition
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath)); // Step 1: two files on disk
var codecs = new RasterCodecs();                              // Step 2: codec layer
var engine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD); // Step 3: engine factory
engine.Startup(codecs, null, null, runtimePath);              // Step 4: blocking startup
// ... still need to create document, add page, call Recognize(), extract text
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs
Imports Leadtools.Forms.DocumentWriters

' LEADTOOLS: Four namespaces, four packages, six steps before recognition
RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath)) ' Step 1: two files on disk
Dim codecs As New RasterCodecs()                             ' Step 2: codec layer
Dim engine As IOcrEngine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD) ' Step 3: engine factory
engine.Startup(codecs, Nothing, Nothing, runtimePath)        ' Step 4: blocking startup
' ... still need to create document, add page, call Recognize(), extract text
$vbLabelText   $csharpLabel
// IronOCR: One namespace, one package, one line
using IronOcr;

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
var text = new IronTesseract().Read("document.jpg").Text;
// IronOCR: One namespace, one package, one line
using IronOcr;

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
var text = new IronTesseract().Read("document.jpg").Text;
Imports IronOcr

IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim text As String = New IronTesseract().Read("document.jpg").Text
$vbLabelText   $csharpLabel

IronOCR과 LEADTOOLS OCR: 기능 비교

다음 표는 두 라이브러리 간의 기능을 직접적으로 매핑합니다.

기능 리드툴즈 OCR IronOCR
NuGet 패키지가 필요합니다. 최소 4개 (PDF의 경우 더 많음) 1 (IronOcr)
라이선스 메커니즘 .LIC + .LIC.KEY 파일 쌍 문자열 키
라이선스 배포 모든 생산 장비에 있는 파일 환경 변수 또는 설정
가격 모델 개발자당 연간 3,000달러~15,000달러 이상(추정치) $999–$2,999 일회성 영구
엔진 초기화 런타임 경로와 함께 수동 Startup() 자동, 게으름
엔진 정지 Shutdown() 전에 수동 Dispose() 필요하지 않음
코덱 레이어 모든 이미지 로딩에 필요한 RasterCodecs 필요하지 않음
PDF 입력 페이지별 래스터화 루프 네이티브 LoadPdf()
비밀번호로 보호된 PDF 별도의 Leadtools.Pdf 모듈 필요 내장된 Password 매개변수
검색 가능한 PDF 출력 DocumentWriter + PdfDocumentOptions + document.Save() result.SaveAsSearchablePdf()
전처리 별도의 명령 클래스 (DeskewCommand, DespeckleCommand 등) OcrInput에 대한 내장 필터 메서드
여러 페이지로 구성된 TIFF 파일 CodecsLoadByteOrder로 수동 프레임 반복 input.LoadImageFrames()
구조화된 출력 페이지 및 영역 수준 페이지, 단락, 줄, 단어, 문자의 좌표
자신감 점수 OcrPageRecognizeStatus enum 백분율로서 result.Confidence
바코드 판독 LEADTOOLS 바코드 모듈 별도 구매 내장 (ReadBarCodes = true)
스레드 안전성 세심한 관리가 필요합니다. (스레드당 하나 IronTesseract)으로 전체
지원되는 언어 60~120 (엔진 종류에 따라 다름) NuGet 언어 패키지를 통해 125개 이상
언어 배포 tessdata files or engine-bundled files 언어별 NuGet 패키지
크로스 플랫폼 플랫폼별 런타임 구성이 지원됩니다. 윈도우, 리눅스, macOS, Docker, Azure, AWS
Docker 배포 .KEY 파일은 마운트되거나 포함되어야 함 표준 dotnet publish
상업적 지원

빠른 시작: LEADTOOLS OCR에서IronOCR로 마이그레이션

1단계: NuGet 패키지 교체

LEADTOOLS 패키지를 모두 제거하세요.

dotnet remove package Leadtools
dotnet remove package Leadtools.Ocr
dotnet remove package Leadtools.Codecs
dotnet remove package Leadtools.Forms.DocumentWriters
dotnet remove package Leadtools.Pdf
dotnet remove package Leadtools
dotnet remove package Leadtools.Ocr
dotnet remove package Leadtools.Codecs
dotnet remove package Leadtools.Forms.DocumentWriters
dotnet remove package Leadtools.Pdf
SHELL

NuGet 에서IronOCR설치하세요.

dotnet add package IronOcr

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

모든 LEADTOOLS using 지시어를 단일IronOCR임포트로 교체하십시오:

// Before (LEADTOOLS)
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

// After (IronOCR)
using IronOcr;
// Before (LEADTOOLS)
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

// After (IronOCR)
using IronOcr;
Imports IronOcr
$vbLabelText   $csharpLabel

단계 3: 라이선스 초기화

모든 RasterSupport.SetLicense() 호출 및 .LIC / .LIC.KEY 파일 참조를 제거하십시오. 애플리케이션 시작 시IronOCR라이선스 키를 추가하세요.

// Single line replaces the entire file-based license setup
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Production pattern: pull from environment variable or secrets manager
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
// Single line replaces the entire file-based license setup
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";

// Production pattern: pull from environment variable or secrets manager
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
Imports System

' Single line replaces the entire file-based license setup
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"

' Production pattern: pull from environment variable or secrets manager
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
$vbLabelText   $csharpLabel

라이선스 키는 appsettings.json, Azure Key Vault, AWS Secrets Manager 또는 Kubernetes 시크릿과 같은 표준 .NET 비밀 관리 패턴에 저장할 수 있습니다. 애플리케이션 실행 파일과 함께 배포할 파일은 없습니다.

코드 마이그레이션 예제

엔진 시동 및 정지 수명주기 제거

LEADTOOLS는 엔진 수명 주기를 명시적으로 관리해야 하므로 엄격한 서비스 클래스 패턴을 요구합니다. 생성자는 엔진을 시작하고, Dispose()는 올바른 순서로 종료하며, Shutdown()Dispose() 전에 건너뛰는 모든 코드 경로는 런타임 오류를 생성합니다.

LEADTOOLS OCR 접근 방식:

using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

// Service class required purely to manage engine lifecycle
public class LeadtoolsOcrService : IDisposable
{
    private IOcrEngine _engine;
    private RasterCodecs _codecs;
    private readonly string _runtimePath;

    public LeadtoolsOcrService(string licPath, string keyPath, string runtimePath)
    {
        _runtimePath = runtimePath;

        // License setup — two files, both must be present
        RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath));

        // Codec layer — required before engine creation
        _codecs = new RasterCodecs();

        // Engine factory — engine type determines capability and cost
        _engine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD);

        // Blocking startup — loads runtime into memory (500–2000ms)
        _engine.Startup(_codecs, null, null, _runtimePath);
    }

    public bool IsReady => _engine?.IsStarted ?? false;

    public string Process(string imagePath)
    {
        if (!IsReady)
            throw new InvalidOperationException("Engine not started");

        using var image = _codecs.Load(imagePath);
        using var doc = _engine.DocumentManager.CreateDocument();
        var page = doc.Pages.AddPage(image, null);
        page.Recognize(null);
        return page.GetText(-1);
    }

    public void Dispose()
    {
        // Order is mandatory: Shutdown before Dispose
        if (_engine?.IsStarted == true)
            _engine.Shutdown();

        _engine?.Dispose();
        _codecs?.Dispose();
    }
}
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

// Service class required purely to manage engine lifecycle
public class LeadtoolsOcrService : IDisposable
{
    private IOcrEngine _engine;
    private RasterCodecs _codecs;
    private readonly string _runtimePath;

    public LeadtoolsOcrService(string licPath, string keyPath, string runtimePath)
    {
        _runtimePath = runtimePath;

        // License setup — two files, both must be present
        RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath));

        // Codec layer — required before engine creation
        _codecs = new RasterCodecs();

        // Engine factory — engine type determines capability and cost
        _engine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD);

        // Blocking startup — loads runtime into memory (500–2000ms)
        _engine.Startup(_codecs, null, null, _runtimePath);
    }

    public bool IsReady => _engine?.IsStarted ?? false;

    public string Process(string imagePath)
    {
        if (!IsReady)
            throw new InvalidOperationException("Engine not started");

        using var image = _codecs.Load(imagePath);
        using var doc = _engine.DocumentManager.CreateDocument();
        var page = doc.Pages.AddPage(image, null);
        page.Recognize(null);
        return page.GetText(-1);
    }

    public void Dispose()
    {
        // Order is mandatory: Shutdown before Dispose
        if (_engine?.IsStarted == true)
            _engine.Shutdown();

        _engine?.Dispose();
        _codecs?.Dispose();
    }
}
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs
Imports System
Imports System.IO

' Service class required purely to manage engine lifecycle
Public Class LeadtoolsOcrService
    Implements IDisposable

    Private _engine As IOcrEngine
    Private _codecs As RasterCodecs
    Private ReadOnly _runtimePath As String

    Public Sub New(licPath As String, keyPath As String, runtimePath As String)
        _runtimePath = runtimePath

        ' License setup — two files, both must be present
        RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath))

        ' Codec layer — required before engine creation
        _codecs = New RasterCodecs()

        ' Engine factory — engine type determines capability and cost
        _engine = OcrEngineManager.CreateEngine(OcrEngineType.LEAD)

        ' Blocking startup — loads runtime into memory (500–2000ms)
        _engine.Startup(_codecs, Nothing, Nothing, _runtimePath)
    End Sub

    Public ReadOnly Property IsReady As Boolean
        Get
            Return If(_engine?.IsStarted, False)
        End Get
    End Property

    Public Function Process(imagePath As String) As String
        If Not IsReady Then
            Throw New InvalidOperationException("Engine not started")
        End If

        Using image = _codecs.Load(imagePath)
            Using doc = _engine.DocumentManager.CreateDocument()
                Dim page = doc.Pages.AddPage(image, Nothing)
                page.Recognize(Nothing)
                Return page.GetText(-1)
            End Using
        End Using
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        ' Order is mandatory: Shutdown before Dispose
        If _engine?.IsStarted = True Then
            _engine.Shutdown()
        End If

        _engine?.Dispose()
        _codecs?.Dispose()
    End Sub
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

using IronOcr;

// No lifecycle management needed — the service class becomes trivial
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    // Always ready — no IsStarted check needed
    public string Process(string imagePath) => _ocr.Read(imagePath).Text;

    // No Dispose() needed for the engine
    // No Startup(), no Shutdown(), no codec layer
}
using IronOcr;

// No lifecycle management needed — the service class becomes trivial
public class OcrService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    // Always ready — no IsStarted check needed
    public string Process(string imagePath) => _ocr.Read(imagePath).Text;

    // No Dispose() needed for the engine
    // No Startup(), no Shutdown(), no codec layer
}
Imports IronOcr

' No lifecycle management needed — the service class becomes trivial
Public Class OcrService
    Private ReadOnly _ocr As New IronTesseract()

    ' Always ready — no IsStarted check needed
    Public Function Process(imagePath As String) As String
        Return _ocr.Read(imagePath).Text
    End Function

    ' No Dispose() needed for the engine
    ' No Startup(), no Shutdown(), no codec layer
End Class
$vbLabelText   $csharpLabel

IronTesseract 인스턴스는 최초 사용 시 초기화됩니다. 생성자 인수가 없으며, 런타임 경로가 없으며, Startup() 호출이 없습니다. 위의 서비스 클래스는 전혀 IDisposable를 구현할 필요가 없습니다. 엔진은 상태가 없으며, 각 Read() 호출 내에서 사용되는 OcrInput 객체는 표준 using 패턴을 통해 자체 정리를 처리합니다. IronTesseract 설정 가이드에서는 언어 선택 및 프로덕션 시나리오에 대한 성능 튜닝을 포함한 구성 옵션을 다룹니다.

다중 프레임 TIFF 일괄 처리

LEADTOOLS는 코덱에서 총 프레임 수를 쿼리 한 다음, 각 _codecs.Load() 호출에서 명시적인 lastPage 매개변수로 반복하여 다중 프레임 TIFF 파일을 처리합니다. 모든 프레임 이미지는 수동으로 삭제해야 하며, 그렇지 않으면 메모리에 누적됩니다.

LEADTOOLS OCR 접근 방식:

using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsTiffBatchService
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public List<string> ProcessMultiFrameTiff(string tiffPath)
    {
        var pageTexts = new List<string>();

        // Must query page count before iterating
        var info = _codecs.GetInformation(tiffPath, true);
        int frameCount = info.TotalPages;

        using var document = _engine.DocumentManager.CreateDocument();

        for (int frameNum = 1; frameNum <= frameCount; frameNum++)
        {
            // Load one frame at a time — must specify firstPage/lastPage
            using var frameImage = _codecs.Load(
                tiffPath,
                0,                            // bitsPerPixel
                CodecsLoadByteOrder.BgrOrGray,
                frameNum,                     // firstPage
                frameNum);                    // lastPage

            var page = document.Pages.AddPage(frameImage, null);
            page.Recognize(null);
            pageTexts.Add(page.GetText(-1));

            // GC pressure accumulates if disposal is missed on any frame
        }

        return pageTexts;
    }

    public Dictionary<string, List<string>> ProcessTiffDirectory(string directoryPath)
    {
        var results = new Dictionary<string, List<string>>();

        foreach (var tiffFile in Directory.GetFiles(directoryPath, "*.tiff"))
        {
            results[tiffFile] = ProcessMultiFrameTiff(tiffFile);

            // Manual GC between files to prevent memory growth
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        return results;
    }
}
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsTiffBatchService
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public List<string> ProcessMultiFrameTiff(string tiffPath)
    {
        var pageTexts = new List<string>();

        // Must query page count before iterating
        var info = _codecs.GetInformation(tiffPath, true);
        int frameCount = info.TotalPages;

        using var document = _engine.DocumentManager.CreateDocument();

        for (int frameNum = 1; frameNum <= frameCount; frameNum++)
        {
            // Load one frame at a time — must specify firstPage/lastPage
            using var frameImage = _codecs.Load(
                tiffPath,
                0,                            // bitsPerPixel
                CodecsLoadByteOrder.BgrOrGray,
                frameNum,                     // firstPage
                frameNum);                    // lastPage

            var page = document.Pages.AddPage(frameImage, null);
            page.Recognize(null);
            pageTexts.Add(page.GetText(-1));

            // GC pressure accumulates if disposal is missed on any frame
        }

        return pageTexts;
    }

    public Dictionary<string, List<string>> ProcessTiffDirectory(string directoryPath)
    {
        var results = new Dictionary<string, List<string>>();

        foreach (var tiffFile in Directory.GetFiles(directoryPath, "*.tiff"))
        {
            results[tiffFile] = ProcessMultiFrameTiff(tiffFile);

            // Manual GC between files to prevent memory growth
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        return results;
    }
}
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs
Imports System.IO

Public Class LeadtoolsTiffBatchService
    Private ReadOnly _engine As IOcrEngine
    Private ReadOnly _codecs As RasterCodecs

    Public Function ProcessMultiFrameTiff(tiffPath As String) As List(Of String)
        Dim pageTexts As New List(Of String)()

        ' Must query page count before iterating
        Dim info = _codecs.GetInformation(tiffPath, True)
        Dim frameCount As Integer = info.TotalPages

        Using document = _engine.DocumentManager.CreateDocument()
            For frameNum As Integer = 1 To frameCount
                ' Load one frame at a time — must specify firstPage/lastPage
                Using frameImage = _codecs.Load(tiffPath, 0, CodecsLoadByteOrder.BgrOrGray, frameNum, frameNum)
                    Dim page = document.Pages.AddPage(frameImage, Nothing)
                    page.Recognize(Nothing)
                    pageTexts.Add(page.GetText(-1))

                    ' GC pressure accumulates if disposal is missed on any frame
                End Using
            Next
        End Using

        Return pageTexts
    End Function

    Public Function ProcessTiffDirectory(directoryPath As String) As Dictionary(Of String, List(Of String))
        Dim results As New Dictionary(Of String, List(Of String))()

        For Each tiffFile In Directory.GetFiles(directoryPath, "*.tiff")
            results(tiffFile) = ProcessMultiFrameTiff(tiffFile)

            ' Manual GC between files to prevent memory growth
            GC.Collect()
            GC.WaitForPendingFinalizers()
        Next

        Return results
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

using IronOcr;

public class TiffBatchService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public List<string> ProcessMultiFrameTiff(string tiffPath)
    {
        using var input = new OcrInput();
        input.LoadImageFrames(tiffPath);  // All frames loaded automatically

        var result = _ocr.Read(input);

        // Per-frame text available through result.Pages
        return result.Pages.Select(p => p.Text).ToList();
    }

    public Dictionary<string, List<string>> ProcessTiffDirectory(string directoryPath)
    {
        var results = new Dictionary<string, List<string>>();

        foreach (var tiffFile in Directory.GetFiles(directoryPath, "*.tiff"))
        {
            results[tiffFile] = ProcessMultiFrameTiff(tiffFile);
        }

        return results;
    }

    // Parallel processing across files — thread-safe out of the box
    public Dictionary<string, List<string>> ProcessTiffDirectoryParallel(string directoryPath)
    {
        var concurrentResults = new System.Collections.Concurrent.ConcurrentDictionary<string, List<string>>();
        var tiffFiles = Directory.GetFiles(directoryPath, "*.tiff");

        Parallel.ForEach(tiffFiles, tiffFile =>
        {
            using var input = new OcrInput();
            input.LoadImageFrames(tiffFile);
            var result = new IronTesseract().Read(input);
            concurrentResults[tiffFile] = result.Pages.Select(p => p.Text).ToList();
        });

        return new Dictionary<string, List<string>>(concurrentResults);
    }
}
using IronOcr;

public class TiffBatchService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public List<string> ProcessMultiFrameTiff(string tiffPath)
    {
        using var input = new OcrInput();
        input.LoadImageFrames(tiffPath);  // All frames loaded automatically

        var result = _ocr.Read(input);

        // Per-frame text available through result.Pages
        return result.Pages.Select(p => p.Text).ToList();
    }

    public Dictionary<string, List<string>> ProcessTiffDirectory(string directoryPath)
    {
        var results = new Dictionary<string, List<string>>();

        foreach (var tiffFile in Directory.GetFiles(directoryPath, "*.tiff"))
        {
            results[tiffFile] = ProcessMultiFrameTiff(tiffFile);
        }

        return results;
    }

    // Parallel processing across files — thread-safe out of the box
    public Dictionary<string, List<string>> ProcessTiffDirectoryParallel(string directoryPath)
    {
        var concurrentResults = new System.Collections.Concurrent.ConcurrentDictionary<string, List<string>>();
        var tiffFiles = Directory.GetFiles(directoryPath, "*.tiff");

        Parallel.ForEach(tiffFiles, tiffFile =>
        {
            using var input = new OcrInput();
            input.LoadImageFrames(tiffFile);
            var result = new IronTesseract().Read(input);
            concurrentResults[tiffFile] = result.Pages.Select(p => p.Text).ToList();
        });

        return new Dictionary<string, List<string>>(concurrentResults);
    }
}
Imports IronOcr
Imports System.IO
Imports System.Collections.Concurrent
Imports System.Threading.Tasks

Public Class TiffBatchService
    Private ReadOnly _ocr As New IronTesseract()

    Public Function ProcessMultiFrameTiff(tiffPath As String) As List(Of String)
        Using input As New OcrInput()
            input.LoadImageFrames(tiffPath) ' All frames loaded automatically

            Dim result = _ocr.Read(input)

            ' Per-frame text available through result.Pages
            Return result.Pages.Select(Function(p) p.Text).ToList()
        End Using
    End Function

    Public Function ProcessTiffDirectory(directoryPath As String) As Dictionary(Of String, List(Of String))
        Dim results As New Dictionary(Of String, List(Of String))()

        For Each tiffFile In Directory.GetFiles(directoryPath, "*.tiff")
            results(tiffFile) = ProcessMultiFrameTiff(tiffFile)
        Next

        Return results
    End Function

    ' Parallel processing across files — thread-safe out of the box
    Public Function ProcessTiffDirectoryParallel(directoryPath As String) As Dictionary(Of String, List(Of String))
        Dim concurrentResults As New ConcurrentDictionary(Of String, List(Of String))()
        Dim tiffFiles = Directory.GetFiles(directoryPath, "*.tiff")

        Parallel.ForEach(tiffFiles, Sub(tiffFile)
                                        Using input As New OcrInput()
                                            input.LoadImageFrames(tiffFile)
                                            Dim result = New IronTesseract().Read(input)
                                            concurrentResults(tiffFile) = result.Pages.Select(Function(p) p.Text).ToList()
                                        End Using
                                    End Sub)

        Return New Dictionary(Of String, List(Of String))(concurrentResults)
    End Function
End Class
$vbLabelText   $csharpLabel

LoadImageFrames()는 한 번의 호출로 TIFF의 모든 프레임을 읽습니다. 프레임 수 쿼리도 없고, 루프도 없고, 프레임별로 명시적인 폐기 처리도 없습니다. 병렬 버전은 스레드당 하나의 IronTesseract 인스턴스를 생성하며, 이는 올바른 패턴입니다. 전체 스레딩 모델을 위해 멀티스레딩 예제를 참조하세요. TIFF 관련 입력 옵션에 대해서는 TIFF 및 GIF 입력 가이드에서 프레임 선택 및 다중 형식 처리에 대한 내용을 참조하십시오.

문서 작성기 파이프라인 간소화

LEADTOOLS 검색 가능한 PDF 생성에는 엔진에서 DocumentWriter 인스턴스를 구성하고, 출력 유형 및 오버레이 설정과 함께 PdfDocumentOptions 객체를 생성하고, SetOptions()를 통해 옵션을 적용한 다음, 포맷 enum과 함께 document.Save()을 호출해야 합니다. 각 단계는 별개의 객체이며 별도의 API 호출입니다.

LEADTOOLS OCR 접근 방식:

using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

public class LeadtoolsDocumentWriterService
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public void CreateSearchablePdfFromImages(string[] imagePaths, string outputPath)
    {
        using var document = _engine.DocumentManager.CreateDocument();

        foreach (var imagePath in imagePaths)
        {
            using var image = _codecs.Load(imagePath);
            var page = document.Pages.AddPage(image, null);
            page.Recognize(null);
        }

        // DocumentWriter configuration — four properties to set before save
        var pdfOptions = new PdfDocumentOptions
        {
            DocumentType = PdfDocumentType.Pdf,
            ImageOverText = true,     // Image layer visible, text layer searchable
            Linearized = false,
            Title = "Searchable Output"
        };

        // Apply options to the engine's writer instance
        _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);

        // Save with format enum — the format must match the options set above
        document.Save(outputPath, DocumentFormat.Pdf, null);
    }

    public void CreateSearchablePdfFromPdf(string inputPdfPath, string outputPath)
    {
        var pdfInfo = _codecs.GetInformation(inputPdfPath, true);
        using var document = _engine.DocumentManager.CreateDocument();

        for (int i = 1; i <= pdfInfo.TotalPages; i++)
        {
            using var pageImage = _codecs.Load(inputPdfPath, 0,
                CodecsLoadByteOrder.BgrOrGray, i, i);

            var page = document.Pages.AddPage(pageImage, null);
            page.Recognize(null);
        }

        var pdfOptions = new PdfDocumentOptions
        {
            DocumentType = PdfDocumentType.Pdf,
            ImageOverText = true,
            Title = Path.GetFileNameWithoutExtension(inputPdfPath)
        };

        _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);
        document.Save(outputPath, DocumentFormat.Pdf, null);
    }
}
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;
using Leadtools.Forms.DocumentWriters;

public class LeadtoolsDocumentWriterService
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public void CreateSearchablePdfFromImages(string[] imagePaths, string outputPath)
    {
        using var document = _engine.DocumentManager.CreateDocument();

        foreach (var imagePath in imagePaths)
        {
            using var image = _codecs.Load(imagePath);
            var page = document.Pages.AddPage(image, null);
            page.Recognize(null);
        }

        // DocumentWriter configuration — four properties to set before save
        var pdfOptions = new PdfDocumentOptions
        {
            DocumentType = PdfDocumentType.Pdf,
            ImageOverText = true,     // Image layer visible, text layer searchable
            Linearized = false,
            Title = "Searchable Output"
        };

        // Apply options to the engine's writer instance
        _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);

        // Save with format enum — the format must match the options set above
        document.Save(outputPath, DocumentFormat.Pdf, null);
    }

    public void CreateSearchablePdfFromPdf(string inputPdfPath, string outputPath)
    {
        var pdfInfo = _codecs.GetInformation(inputPdfPath, true);
        using var document = _engine.DocumentManager.CreateDocument();

        for (int i = 1; i <= pdfInfo.TotalPages; i++)
        {
            using var pageImage = _codecs.Load(inputPdfPath, 0,
                CodecsLoadByteOrder.BgrOrGray, i, i);

            var page = document.Pages.AddPage(pageImage, null);
            page.Recognize(null);
        }

        var pdfOptions = new PdfDocumentOptions
        {
            DocumentType = PdfDocumentType.Pdf,
            ImageOverText = true,
            Title = Path.GetFileNameWithoutExtension(inputPdfPath)
        };

        _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);
        document.Save(outputPath, DocumentFormat.Pdf, null);
    }
}
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs
Imports Leadtools.Forms.DocumentWriters

Public Class LeadtoolsDocumentWriterService
    Private ReadOnly _engine As IOcrEngine
    Private ReadOnly _codecs As RasterCodecs

    Public Sub CreateSearchablePdfFromImages(imagePaths As String(), outputPath As String)
        Using document = _engine.DocumentManager.CreateDocument()

            For Each imagePath In imagePaths
                Using image = _codecs.Load(imagePath)
                    Dim page = document.Pages.AddPage(image, Nothing)
                    page.Recognize(Nothing)
                End Using
            Next

            ' DocumentWriter configuration — four properties to set before save
            Dim pdfOptions As New PdfDocumentOptions With {
                .DocumentType = PdfDocumentType.Pdf,
                .ImageOverText = True,     ' Image layer visible, text layer searchable
                .Linearized = False,
                .Title = "Searchable Output"
            }

            ' Apply options to the engine's writer instance
            _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions)

            ' Save with format enum — the format must match the options set above
            document.Save(outputPath, DocumentFormat.Pdf, Nothing)
        End Using
    End Sub

    Public Sub CreateSearchablePdfFromPdf(inputPdfPath As String, outputPath As String)
        Dim pdfInfo = _codecs.GetInformation(inputPdfPath, True)
        Using document = _engine.DocumentManager.CreateDocument()

            For i As Integer = 1 To pdfInfo.TotalPages
                Using pageImage = _codecs.Load(inputPdfPath, 0, CodecsLoadByteOrder.BgrOrGray, i, i)
                    Dim page = document.Pages.AddPage(pageImage, Nothing)
                    page.Recognize(Nothing)
                End Using
            Next

            Dim pdfOptions As New PdfDocumentOptions With {
                .DocumentType = PdfDocumentType.Pdf,
                .ImageOverText = True,
                .Title = Path.GetFileNameWithoutExtension(inputPdfPath)
            }

            _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions)
            document.Save(outputPath, DocumentFormat.Pdf, Nothing)
        End Using
    End Sub
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

using IronOcr;

public class SearchablePdfService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public void CreateSearchablePdfFromImages(string[] imagePaths, string outputPath)
    {
        using var input = new OcrInput();
        foreach (var imagePath in imagePaths)
            input.LoadImage(imagePath);

        var result = _ocr.Read(input);
        result.SaveAsSearchablePdf(outputPath);  // DocumentWriter pipeline: gone
    }

    public void CreateSearchablePdfFromPdf(string inputPdfPath, string outputPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(inputPdfPath);

        var result = _ocr.Read(input);
        result.SaveAsSearchablePdf(outputPath);
    }

    // Get bytes directly — useful for streaming responses in ASP.NET
    public byte[] CreateSearchablePdfBytes(string inputPdfPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(inputPdfPath);
        return _ocr.Read(input).SaveAsSearchablePdfBytes();
    }
}
using IronOcr;

public class SearchablePdfService
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public void CreateSearchablePdfFromImages(string[] imagePaths, string outputPath)
    {
        using var input = new OcrInput();
        foreach (var imagePath in imagePaths)
            input.LoadImage(imagePath);

        var result = _ocr.Read(input);
        result.SaveAsSearchablePdf(outputPath);  // DocumentWriter pipeline: gone
    }

    public void CreateSearchablePdfFromPdf(string inputPdfPath, string outputPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(inputPdfPath);

        var result = _ocr.Read(input);
        result.SaveAsSearchablePdf(outputPath);
    }

    // Get bytes directly — useful for streaming responses in ASP.NET
    public byte[] CreateSearchablePdfBytes(string inputPdfPath)
    {
        using var input = new OcrInput();
        input.LoadPdf(inputPdfPath);
        return _ocr.Read(input).SaveAsSearchablePdfBytes();
    }
}
Imports IronOcr

Public Class SearchablePdfService
    Private ReadOnly _ocr As New IronTesseract()

    Public Sub CreateSearchablePdfFromImages(imagePaths As String(), outputPath As String)
        Using input As New OcrInput()
            For Each imagePath In imagePaths
                input.LoadImage(imagePath)
            Next

            Dim result = _ocr.Read(input)
            result.SaveAsSearchablePdf(outputPath) ' DocumentWriter pipeline: gone
        End Using
    End Sub

    Public Sub CreateSearchablePdfFromPdf(inputPdfPath As String, outputPath As String)
        Using input As New OcrInput()
            input.LoadPdf(inputPdfPath)

            Dim result = _ocr.Read(input)
            result.SaveAsSearchablePdf(outputPath)
        End Using
    End Sub

    ' Get bytes directly — useful for streaming responses in ASP.NET
    Public Function CreateSearchablePdfBytes(inputPdfPath As String) As Byte()
        Using input As New OcrInput()
            input.LoadPdf(inputPdfPath)
            Return _ocr.Read(input).SaveAsSearchablePdfBytes()
        End Using
    End Function
End Class
$vbLabelText   $csharpLabel

SaveAsSearchablePdf()는 전체 PdfDocumentOptions + SetOptions() + document.Save() 체인을 대체합니다. 이미지가 텍스트 위에 겹쳐지는 레이어 동작은 자동으로 이루어집니다. 검색 가능한 PDF 출력에 대한 전체 문서는 검색 가능한 PDF 사용 방법 가이드에서 출력 옵션을 다루고 , 검색 가능한 PDF 예제에서 ASP.NET 응답 스트림과의 통합을 보여줍니다.

다중 영역 필드 추출 마이그레이션

LEADTOOLS 영역 기반 OCR은 LeadRect 경계, OcrZoneTypeOcrZoneCharacterFilters 속성과 함께 OcrZone 객체를 사용합니다. 여러 영역을 한 페이지에 추가하고, 한 번의 page.Recognize() 호출에서 인식한 후, 영역 인덱스를 통해 추출합니다. 영역 인덱스는 삽입 순서와 일치하므로 추출 루프는 해당 순서를 유지해야 합니다.

LEADTOOLS OCR 접근 방식:

using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsFormFieldExtractor
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    // Invoice field extraction using named zones
    public InvoiceFields ExtractInvoiceFields(string invoicePath)
    {
        using var image = _codecs.Load(invoicePath);
        using var document = _engine.DocumentManager.CreateDocument();
        var page = document.Pages.AddPage(image, null);

        // Must clear auto-detected zones before adding custom ones
        page.Zones.Clear();

        // Zone definitions — index order matters for extraction
        var zoneDefinitions = new[]
        {
            new { Name = "InvoiceNumber", X = 450, Y = 80,  W = 200, H = 30 },
            new { Name = "InvoiceDate",   X = 450, Y = 115, W = 200, H = 30 },
            new { Name = "VendorName",    X = 50,  Y = 150, W = 300, H = 40 },
            new { Name = "TotalAmount",   X = 450, Y = 600, W = 200, H = 30 }
        };

        foreach (var def in zoneDefinitions)
        {
            var zone = new OcrZone
            {
                Bounds = new LeadRect(def.X, def.Y, def.W, def.H),
                ZoneType = OcrZoneType.Text,
                CharacterFilters = OcrZoneCharacterFilters.None,
                RecognitionModule = OcrZoneRecognitionModule.Auto
            };
            page.Zones.Add(zone);
        }

        page.Recognize(null);

        // Extract by index — must match insertion order exactly
        return new InvoiceFields
        {
            InvoiceNumber = page.Zones[0].Text?.Trim(),
            InvoiceDate   = page.Zones[1].Text?.Trim(),
            VendorName    = page.Zones[2].Text?.Trim(),
            TotalAmount   = page.Zones[3].Text?.Trim()
        };
    }
}

public class InvoiceFields
{
    public string InvoiceNumber { get; set; }
    public string InvoiceDate   { get; set; }
    public string VendorName    { get; set; }
    public string TotalAmount   { get; set; }
}
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsFormFieldExtractor
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    // Invoice field extraction using named zones
    public InvoiceFields ExtractInvoiceFields(string invoicePath)
    {
        using var image = _codecs.Load(invoicePath);
        using var document = _engine.DocumentManager.CreateDocument();
        var page = document.Pages.AddPage(image, null);

        // Must clear auto-detected zones before adding custom ones
        page.Zones.Clear();

        // Zone definitions — index order matters for extraction
        var zoneDefinitions = new[]
        {
            new { Name = "InvoiceNumber", X = 450, Y = 80,  W = 200, H = 30 },
            new { Name = "InvoiceDate",   X = 450, Y = 115, W = 200, H = 30 },
            new { Name = "VendorName",    X = 50,  Y = 150, W = 300, H = 40 },
            new { Name = "TotalAmount",   X = 450, Y = 600, W = 200, H = 30 }
        };

        foreach (var def in zoneDefinitions)
        {
            var zone = new OcrZone
            {
                Bounds = new LeadRect(def.X, def.Y, def.W, def.H),
                ZoneType = OcrZoneType.Text,
                CharacterFilters = OcrZoneCharacterFilters.None,
                RecognitionModule = OcrZoneRecognitionModule.Auto
            };
            page.Zones.Add(zone);
        }

        page.Recognize(null);

        // Extract by index — must match insertion order exactly
        return new InvoiceFields
        {
            InvoiceNumber = page.Zones[0].Text?.Trim(),
            InvoiceDate   = page.Zones[1].Text?.Trim(),
            VendorName    = page.Zones[2].Text?.Trim(),
            TotalAmount   = page.Zones[3].Text?.Trim()
        };
    }
}

public class InvoiceFields
{
    public string InvoiceNumber { get; set; }
    public string InvoiceDate   { get; set; }
    public string VendorName    { get; set; }
    public string TotalAmount   { get; set; }
}
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs

Public Class LeadtoolsFormFieldExtractor
    Private ReadOnly _engine As IOcrEngine
    Private ReadOnly _codecs As RasterCodecs

    ' Invoice field extraction using named zones
    Public Function ExtractInvoiceFields(invoicePath As String) As InvoiceFields
        Using image = _codecs.Load(invoicePath)
            Using document = _engine.DocumentManager.CreateDocument()
                Dim page = document.Pages.AddPage(image, Nothing)

                ' Must clear auto-detected zones before adding custom ones
                page.Zones.Clear()

                ' Zone definitions — index order matters for extraction
                Dim zoneDefinitions = New() {
                    New With {.Name = "InvoiceNumber", .X = 450, .Y = 80, .W = 200, .H = 30},
                    New With {.Name = "InvoiceDate", .X = 450, .Y = 115, .W = 200, .H = 30},
                    New With {.Name = "VendorName", .X = 50, .Y = 150, .W = 300, .H = 40},
                    New With {.Name = "TotalAmount", .X = 450, .Y = 600, .W = 200, .H = 30}
                }

                For Each def In zoneDefinitions
                    Dim zone = New OcrZone With {
                        .Bounds = New LeadRect(def.X, def.Y, def.W, def.H),
                        .ZoneType = OcrZoneType.Text,
                        .CharacterFilters = OcrZoneCharacterFilters.None,
                        .RecognitionModule = OcrZoneRecognitionModule.Auto
                    }
                    page.Zones.Add(zone)
                Next

                page.Recognize(Nothing)

                ' Extract by index — must match insertion order exactly
                Return New InvoiceFields With {
                    .InvoiceNumber = page.Zones(0).Text?.Trim(),
                    .InvoiceDate = page.Zones(1).Text?.Trim(),
                    .VendorName = page.Zones(2).Text?.Trim(),
                    .TotalAmount = page.Zones(3).Text?.Trim()
                }
            End Using
        End Using
    End Function
End Class

Public Class InvoiceFields
    Public Property InvoiceNumber As String
    Public Property InvoiceDate As String
    Public Property VendorName As String
    Public Property TotalAmount As String
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

using IronOcr;

public class FormFieldExtractor
{
    private readonly IronTesseract _ocr = new IronTesseract();

    // Each field gets its own CropRectangle-scoped Read() call
    // No zone index management, no zone ordering dependency
    public InvoiceFields ExtractInvoiceFields(string invoicePath)
    {
        return new InvoiceFields
        {
            InvoiceNumber = ReadRegion(invoicePath, 450, 80,  200, 30),
            InvoiceDate   = ReadRegion(invoicePath, 450, 115, 200, 30),
            VendorName    = ReadRegion(invoicePath, 50,  150, 300, 40),
            TotalAmount   = ReadRegion(invoicePath, 450, 600, 200, 30)
        };
    }

    private string ReadRegion(string imagePath, int x, int y, int width, int height)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, new CropRectangle(x, y, width, height));
        return _ocr.Read(input).Text.Trim();
    }

    // Batch: extract the same field from many invoices in parallel
    public Dictionary<string, string> ExtractInvoiceNumbersBatch(string[] invoicePaths)
    {
        var results = new System.Collections.Concurrent.ConcurrentDictionary<string, string>();

        Parallel.ForEach(invoicePaths, invoicePath =>
        {
            using var input = new OcrInput();
            input.LoadImage(invoicePath, new CropRectangle(450, 80, 200, 30));
            results[invoicePath] = new IronTesseract().Read(input).Text.Trim();
        });

        return new Dictionary<string, string>(results);
    }
}
using IronOcr;

public class FormFieldExtractor
{
    private readonly IronTesseract _ocr = new IronTesseract();

    // Each field gets its own CropRectangle-scoped Read() call
    // No zone index management, no zone ordering dependency
    public InvoiceFields ExtractInvoiceFields(string invoicePath)
    {
        return new InvoiceFields
        {
            InvoiceNumber = ReadRegion(invoicePath, 450, 80,  200, 30),
            InvoiceDate   = ReadRegion(invoicePath, 450, 115, 200, 30),
            VendorName    = ReadRegion(invoicePath, 50,  150, 300, 40),
            TotalAmount   = ReadRegion(invoicePath, 450, 600, 200, 30)
        };
    }

    private string ReadRegion(string imagePath, int x, int y, int width, int height)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, new CropRectangle(x, y, width, height));
        return _ocr.Read(input).Text.Trim();
    }

    // Batch: extract the same field from many invoices in parallel
    public Dictionary<string, string> ExtractInvoiceNumbersBatch(string[] invoicePaths)
    {
        var results = new System.Collections.Concurrent.ConcurrentDictionary<string, string>();

        Parallel.ForEach(invoicePaths, invoicePath =>
        {
            using var input = new OcrInput();
            input.LoadImage(invoicePath, new CropRectangle(450, 80, 200, 30));
            results[invoicePath] = new IronTesseract().Read(input).Text.Trim();
        });

        return new Dictionary<string, string>(results);
    }
}
Imports IronOcr
Imports System.Collections.Concurrent
Imports System.Threading.Tasks

Public Class FormFieldExtractor
    Private ReadOnly _ocr As New IronTesseract()

    ' Each field gets its own CropRectangle-scoped Read() call
    ' No zone index management, no zone ordering dependency
    Public Function ExtractInvoiceFields(invoicePath As String) As InvoiceFields
        Return New InvoiceFields With {
            .InvoiceNumber = ReadRegion(invoicePath, 450, 80, 200, 30),
            .InvoiceDate = ReadRegion(invoicePath, 450, 115, 200, 30),
            .VendorName = ReadRegion(invoicePath, 50, 150, 300, 40),
            .TotalAmount = ReadRegion(invoicePath, 450, 600, 200, 30)
        }
    End Function

    Private Function ReadRegion(imagePath As String, x As Integer, y As Integer, width As Integer, height As Integer) As String
        Using input As New OcrInput()
            input.LoadImage(imagePath, New CropRectangle(x, y, width, height))
            Return _ocr.Read(input).Text.Trim()
        End Using
    End Function

    ' Batch: extract the same field from many invoices in parallel
    Public Function ExtractInvoiceNumbersBatch(invoicePaths As String()) As Dictionary(Of String, String)
        Dim results As New ConcurrentDictionary(Of String, String)()

        Parallel.ForEach(invoicePaths, Sub(invoicePath)
                                           Using input As New OcrInput()
                                               input.LoadImage(invoicePath, New CropRectangle(450, 80, 200, 30))
                                               results(invoicePath) = New IronTesseract().Read(input).Text.Trim()
                                           End Using
                                       End Sub)

        Return New Dictionary(Of String, String)(results)
    End Function
End Class
$vbLabelText   $csharpLabel

CropRectangleLoadImage()에 직접 전달하여 OcrZone 설정을 전부 대체합니다. 추적할 영역 인덱스가 없고, page.Zones.Clear() 호출이 필요 없으며, 인식 상태 확인도 필요하지 않습니다. 영역 기반 OCR 가이드는 단일 영역 및 다중 영역 추출 패턴을 다룹니다. 송장 필드 추출에 대한 전체 튜토리얼은 송장 OCR 튜토리얼을 참조하세요.

단어 좌표를 이용한 구조화된 데이터 추출

LEADTOOLS의 구조화된 출력은 페이지 및 영역 수준에서 작동합니다. 경계 상자 좌표로 단어 수준의 데이터를 얻으려면, 개발자는 인식된 영역 내에서 OcrWord 객체에 접근합니다. API를 사용하려면 인식 후 영역 컬렉션을 처리하고 영역별 단어 목록을 반복해야 합니다.

LEADTOOLS OCR 접근 방식:

using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsStructuredExtractor
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public List<WordLocation> ExtractWordsWithLocations(string imagePath)
    {
        var wordLocations = new List<WordLocation>();

        using var image = _codecs.Load(imagePath);
        using var document = _engine.DocumentManager.CreateDocument();
        var page = document.Pages.AddPage(image, null);
        page.Recognize(null);

        // Access words through the zone collection
        foreach (OcrZone zone in page.Zones)
        {
            foreach (OcrWord word in zone.Words)
            {
                wordLocations.Add(new WordLocation
                {
                    Text       = word.Value,
                    X          = word.Bounds.X,
                    Y          = word.Bounds.Y,
                    Width      = word.Bounds.Width,
                    Height     = word.Bounds.Height,
                    Confidence = word.Confidence
                });
            }
        }

        return wordLocations;
    }
}

public class WordLocation
{
    public string Text       { get; set; }
    public int    X          { get; set; }
    public int    Y          { get; set; }
    public int    Width      { get; set; }
    public int    Height     { get; set; }
    public int    Confidence { get; set; }
}
using Leadtools;
using Leadtools.Ocr;
using Leadtools.Codecs;

public class LeadtoolsStructuredExtractor
{
    private readonly IOcrEngine _engine;
    private readonly RasterCodecs _codecs;

    public List<WordLocation> ExtractWordsWithLocations(string imagePath)
    {
        var wordLocations = new List<WordLocation>();

        using var image = _codecs.Load(imagePath);
        using var document = _engine.DocumentManager.CreateDocument();
        var page = document.Pages.AddPage(image, null);
        page.Recognize(null);

        // Access words through the zone collection
        foreach (OcrZone zone in page.Zones)
        {
            foreach (OcrWord word in zone.Words)
            {
                wordLocations.Add(new WordLocation
                {
                    Text       = word.Value,
                    X          = word.Bounds.X,
                    Y          = word.Bounds.Y,
                    Width      = word.Bounds.Width,
                    Height     = word.Bounds.Height,
                    Confidence = word.Confidence
                });
            }
        }

        return wordLocations;
    }
}

public class WordLocation
{
    public string Text       { get; set; }
    public int    X          { get; set; }
    public int    Y          { get; set; }
    public int    Width      { get; set; }
    public int    Height     { get; set; }
    public int    Confidence { get; set; }
}
Imports Leadtools
Imports Leadtools.Ocr
Imports Leadtools.Codecs

Public Class LeadtoolsStructuredExtractor
    Private ReadOnly _engine As IOcrEngine
    Private ReadOnly _codecs As RasterCodecs

    Public Function ExtractWordsWithLocations(imagePath As String) As List(Of WordLocation)
        Dim wordLocations As New List(Of WordLocation)()

        Using image = _codecs.Load(imagePath)
            Using document = _engine.DocumentManager.CreateDocument()
                Dim page = document.Pages.AddPage(image, Nothing)
                page.Recognize(Nothing)

                ' Access words through the zone collection
                For Each zone As OcrZone In page.Zones
                    For Each word As OcrWord In zone.Words
                        wordLocations.Add(New WordLocation With {
                            .Text = word.Value,
                            .X = word.Bounds.X,
                            .Y = word.Bounds.Y,
                            .Width = word.Bounds.Width,
                            .Height = word.Bounds.Height,
                            .Confidence = word.Confidence
                        })
                    Next
                Next
            End Using
        End Using

        Return wordLocations
    End Function
End Class

Public Class WordLocation
    Public Property Text As String
    Public Property X As Integer
    Public Property Y As Integer
    Public Property Width As Integer
    Public Property Height As Integer
    Public Property Confidence As Integer
End Class
$vbLabelText   $csharpLabel

IronOCR 접근 방식:

using IronOcr;

public class StructuredExtractor
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public List<WordLocation> ExtractWordsWithLocations(string imagePath)
    {
        var result = _ocr.Read(imagePath);

        // Five-level hierarchy: Pages > Paragraphs > Lines > Words > Characters
        return result.Pages
            .SelectMany(page => page.Paragraphs)
            .SelectMany(para => para.Lines)
            .SelectMany(line => line.Words)
            .Select(word => new WordLocation
            {
                Text       = word.Text,
                X          = word.X,
                Y          = word.Y,
                Width      = word.Width,
                Height     = word.Height,
                Confidence = (int)word.Confidence
            })
            .ToList();
    }

    // Paragraph-level extraction with position data
    public void PrintDocumentStructure(string imagePath)
    {
        var result = _ocr.Read(imagePath);
        Console.WriteLine($"Document confidence: {result.Confidence}%");

        foreach (var page in result.Pages)
        {
            Console.WriteLine($"Page {page.PageNumber}:");
            foreach (var paragraph in page.Paragraphs)
            {
                Console.WriteLine($"  Paragraph at ({paragraph.X}, {paragraph.Y}):");
                Console.WriteLine($"  {paragraph.Text}");
            }
        }
    }
}
using IronOcr;

public class StructuredExtractor
{
    private readonly IronTesseract _ocr = new IronTesseract();

    public List<WordLocation> ExtractWordsWithLocations(string imagePath)
    {
        var result = _ocr.Read(imagePath);

        // Five-level hierarchy: Pages > Paragraphs > Lines > Words > Characters
        return result.Pages
            .SelectMany(page => page.Paragraphs)
            .SelectMany(para => para.Lines)
            .SelectMany(line => line.Words)
            .Select(word => new WordLocation
            {
                Text       = word.Text,
                X          = word.X,
                Y          = word.Y,
                Width      = word.Width,
                Height     = word.Height,
                Confidence = (int)word.Confidence
            })
            .ToList();
    }

    // Paragraph-level extraction with position data
    public void PrintDocumentStructure(string imagePath)
    {
        var result = _ocr.Read(imagePath);
        Console.WriteLine($"Document confidence: {result.Confidence}%");

        foreach (var page in result.Pages)
        {
            Console.WriteLine($"Page {page.PageNumber}:");
            foreach (var paragraph in page.Paragraphs)
            {
                Console.WriteLine($"  Paragraph at ({paragraph.X}, {paragraph.Y}):");
                Console.WriteLine($"  {paragraph.Text}");
            }
        }
    }
}
Imports IronOcr

Public Class StructuredExtractor
    Private ReadOnly _ocr As New IronTesseract()

    Public Function ExtractWordsWithLocations(imagePath As String) As List(Of WordLocation)
        Dim result = _ocr.Read(imagePath)

        ' Five-level hierarchy: Pages > Paragraphs > Lines > Words > Characters
        Return result.Pages _
            .SelectMany(Function(page) page.Paragraphs) _
            .SelectMany(Function(para) para.Lines) _
            .SelectMany(Function(line) line.Words) _
            .Select(Function(word) New WordLocation With {
                .Text = word.Text,
                .X = word.X,
                .Y = word.Y,
                .Width = word.Width,
                .Height = word.Height,
                .Confidence = CInt(word.Confidence)
            }) _
            .ToList()
    End Function

    ' Paragraph-level extraction with position data
    Public Sub PrintDocumentStructure(imagePath As String)
        Dim result = _ocr.Read(imagePath)
        Console.WriteLine($"Document confidence: {result.Confidence}%")

        For Each page In result.Pages
            Console.WriteLine($"Page {page.PageNumber}:")
            For Each paragraph In page.Paragraphs
                Console.WriteLine($"  Paragraph at ({paragraph.X}, {paragraph.Y}):")
                Console.WriteLine($"  {paragraph.Text}")
            Next
        Next
    End Sub
End Class
$vbLabelText   $csharpLabel

IronOCR의 결과 계층 구조는 Pages에서 시작하여 Paragraphs, Lines, WordsCharacters으로 내려갑니다. 각 수준은 X, Y, Width, Height, TextConfidence를 노출합니다. LEADTOOLS의 영역 기반 액세스 패턴이 사라집니다. 워드 데이터에 접근하기 위해 영역을 반복할 필요가 없습니다. 읽기 결과 사용 설명서는 좌표 접근 패턴을 포함한 전체 출력 모델을 다룹니다. OcrResult API 참조 문서에는 결과 계층 구조의 모든 속성이 설명되어 있습니다.

리드툴즈 OCR API와IronOCR매핑 참조

리드툴즈 OCR IronOCR
RasterSupport.SetLicense(licPath, keyContent) IronOcr.License.LicenseKey = "key"
new RasterCodecs() 필요하지 않음
OcrEngineManager.CreateEngine(OcrEngineType.LEAD) new IronTesseract()
engine.Startup(codecs, null, null, runtimePath) 필요하지 않음
engine.IsStarted 필수 사항이 아닙니다 (항상 준비되어 있습니다).
engine.Shutdown() 필요하지 않음
engine.Dispose() 필요하지 않음
_codecs.Load(imagePath) input.LoadImage(imagePath)
_codecs.Load(path, 0, BgrOrGray, page, page) input.LoadPdf(path) 또는 input.LoadImageFrames(path)
_codecs.GetInformation(path, true).TotalPages 필요 없음 — 자동
engine.DocumentManager.CreateDocument() 필요하지 않음
document.Pages.AddPage(image, null) input.LoadImage(imagePath)
page.Recognize(null) ocr.Read(input) (인식은 Read()의 일부)
page.GetText(-1) result.Text
page.RecognizeStatus result.Confidence (백분율)
OcrZone { Bounds = new LeadRect(x, y, w, h) } new CropRectangle(x, y, w, h)
page.Zones.Clear() 필요하지 않음
page.Zones.Add(zone) input.LoadImage(path, cropRect)
zone.Words / word.Bounds result.Pages[n].Words / word.X, word.Y
DeskewCommand().Run(image) input.Deskew()
DespeckleCommand().Run(image) input.DeNoise()
AutoBinarizeCommand().Run(image) input.Binarize()
ContrastBrightnessCommand().Run(image) input.Contrast()
new PdfDocumentOptions { ImageOverText = true } SaveAsSearchablePdf()에 의해 자동으로 처리됩니다.
engine.DocumentWriterInstance.SetOptions(format, opts) 필요하지 않음
document.Save(path, DocumentFormat.Pdf, null) result.SaveAsSearchablePdf(path)
_codecs.Options.Pdf.Load.Password = password input.LoadPdf(path, Password: password)
RasterSupport.IsLocked(RasterSupportType.Document) IronOcr.License.IsLicensed

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

문제 1: 라이선스 파일 경로 확인 실패

LEADTOOLS: RasterSupport.SetLicense()은 작업 디렉토리를 기준으로 .LIC.LIC.KEY 파일 경로를 해결하며, 이는 bin/Debug, bin/Release, Docker 컨테이너 및 IIS 애플리케이션 풀 간에 다릅니다. 일반적인 실패 모드는 개발에서 작동하지만, 작업 디렉토리가 변경되어 생산에서 "License file not found"를 던지는 경로입니다.

해결책: 두 라이선스 파일과 SetLicense() 호출을 완전히 삭제하십시오. 환경 변수에서 읽어오는 단일 문자열 할당으로 대체하세요.

// Remove this:
// RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath));

// Replace with this:
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
    ?? throw new InvalidOperationException("IRONOCR_LICENSE environment variable not set");
// Remove this:
// RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath));

// Replace with this:
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
    ?? throw new InvalidOperationException("IRONOCR_LICENSE environment variable not set");
' Remove this:
' RasterSupport.SetLicense(licPath, File.ReadAllText(keyPath))

' Replace with this:
IronOcr.License.LicenseKey = If(Environment.GetEnvironmentVariable("IRONOCR_LICENSE"), Throw New InvalidOperationException("IRONOCR_LICENSE environment variable not set"))
$vbLabelText   $csharpLabel

문자열 키는 모든 환경에서 동일하게 동작합니다. 데이터베이스 연결 문자열에 이미 사용 중인 동일한 비밀 관리자에 저장하십시오.

문제 2: 엔진 시동 실패 예외

LEADTOOLS: engine.Startup() 완료 전에 또는 engine.Shutdown() 호출 후에 (예: 종료 중 사용하는 이전 경합 조건에서) 인식 메서드를 호출하면 "엔진이 시작되지 않았습니다."라는 메시지와 함께 InvalidOperationException를 던집니다. 장기 서비스를 제공하는 클래스는 IsStarted 검사를 통해 이를 방지해야 합니다.

해결책: IronTesseract는 시작 호출이 필요 없으며 시작/중지 상태가 없습니다. IsStarted 보호자와 전체 수명 주기 서비스 클래스를 삭제할 수 있습니다:

// Remove the guard:
// if (!_engine.IsStarted)
//     throw new InvalidOperationException("Engine not started");

// IronTesseract is always ready — just call Read()
var result = _ocr.Read(imagePath);
// Remove the guard:
// if (!_engine.IsStarted)
//     throw new InvalidOperationException("Engine not started");

// IronTesseract is always ready — just call Read()
var result = _ocr.Read(imagePath);
$vbLabelText   $csharpLabel

문제 3: 일괄 처리 시 래스터 이미지 메모리 누적 문제

LEADTOOLS: PDF 페이지 또는 이미지 파일을 반복하는 배치 처리는 루프에서 RasterImage 인스턴스를 생성합니다. 예외가 using 블록 종료 전에 발생하거나, 호출이 누락된 수동 폐기 패턴으로 인해 RasterImage를 처리하지 못하는 코드 경로가 발생하면, 해제되지 않은 이미지가 메모리에 축적됩니다. LEADTOOLS 생산 코드는 일반적으로 배치 사이에 보상 메커니즘으로 GC.Collect() / GC.WaitForPendingFinalizers() 호출을 포함합니다.

해결책: 모든 GC.Collect() 호출을 제거하십시오. OcrInput은IronOCR파이프라인에서 유일한 IDisposable이며, 표준 using 블록을 사용하여 각 배치 작업에 범위가 지정됩니다:

// Remove this pattern:
// GC.Collect();
// GC.WaitForPendingFinalizers();

// Replace with standard using scope:
foreach (var filePath in filePaths)
{
    using var input = new OcrInput();
    input.LoadImage(filePath);
    var text = _ocr.Read(input).Text;
    // input disposed here — no accumulation
}
// Remove this pattern:
// GC.Collect();
// GC.WaitForPendingFinalizers();

// Replace with standard using scope:
foreach (var filePath in filePaths)
{
    using var input = new OcrInput();
    input.LoadImage(filePath);
    var text = _ocr.Read(input).Text;
    // input disposed here — no accumulation
}
' Remove this pattern:
' GC.Collect()
' GC.WaitForPendingFinalizers()

' Replace with standard using scope:
For Each filePath In filePaths
    Using input As New OcrInput()
        input.LoadImage(filePath)
        Dim text = _ocr.Read(input).Text
        ' input disposed here — no accumulation
    End Using
Next
$vbLabelText   $csharpLabel

메모리 최적화에 대한 추가적인 지침은 메모리 할당 감소 관련 블로그 게시물을 참조하세요.

문제 4: DocumentWriterInstance 옵션이 호출 간에 유지됨

LEADTOOLS: engine.DocumentWriterInstance.SetOptions()는 공유 엔진 작성자 인스턴스의 상태를 수정합니다. 어떤 코드 경로가 PdfDocumentOptionsDocumentType = PdfDocumentType.PdfA로 설정하고 후속 호출이 document.Save()를 호출하기 전에 해당 옵션을 재설정하지 않으면, 이전 호출의 출력 형식이 유지됩니다. 이는 공유 엔진 인스턴스에 상태를 유지하는 부작용입니다.

해결책:IronOCR공유되는 기록자 상태가 없습니다. 각 SaveAsSearchablePdf() 호출은 독립적입니다.

// Remove the options setup:
// var pdfOptions = new PdfDocumentOptions { ... };
// _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);
// document.Save(outputPath, DocumentFormat.Pdf, null);

// Replace with:
result.SaveAsSearchablePdf(outputPath);
// Remove the options setup:
// var pdfOptions = new PdfDocumentOptions { ... };
// _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions);
// document.Save(outputPath, DocumentFormat.Pdf, null);

// Replace with:
result.SaveAsSearchablePdf(outputPath);
' Remove the options setup:
' Dim pdfOptions As New PdfDocumentOptions With { ... }
' _engine.DocumentWriterInstance.SetOptions(DocumentFormat.Pdf, pdfOptions)
' document.Save(outputPath, DocumentFormat.Pdf, Nothing)

' Replace with:
result.SaveAsSearchablePdf(outputPath)
$vbLabelText   $csharpLabel

각 호출은 이미지 오버레이와 검색 가능한 텍스트 레이어가 포함된 표준 PDF 출력을 생성합니다. 통화 간 재설정할 공유 옵션 상태가 없습니다.

문제 5: 잘못된 번들 - 런타임 시 모듈 누락

LEADTOOLS: 문서 이미징 SDK를 구매한 팀은 Leadtools.Pdf 유형을 사용할 수 없거나, 암호화된 PDF 페이지가 RasterExceptionRasterExceptionCode.FeatureNotSupported를 가진 예외를 던지는 것을 발견할 수 있습니다. 이 문제는 구매한 패키지에 PDF 모듈이 포함되어 있지 않은 경우에 발생하며, 프로덕션 환경에서 런타임 시에만 오류가 나타납니다.

해결책:IronOCR모든 라이선스 등급에 모든 기능을 포함합니다. 별도의 PDF 모듈, 암호화된 문서를 위한 애드온, 또는 더 높은 정확도의 엔진을 위한 부수적인 벤더는 없습니다. 단일 IronOcr 패키지를 설치한 후에는 추가 구매 없이 전체 기능 세트가 사용할 수 있습니다:

dotnet add package IronOcr

문제 6: 재정렬 후 영역 인덱스 불일치

LEADTOOLS: 영역 추출은 삽입 순서에 추출 논리를 연결하는 포지셔널 인덱싱 — page.Zones[0].Text, page.Zones[1].Text — 을 사용합니다. 변경된 양식 레이아웃에 맞춰 영역 정의 순서를 재정렬하면 이후의 모든 인덱스가 이동하여 추출이 자동으로 중단됩니다.

해결책: IronOCR는 필드별 CropRectangle와 함께 명명된 변수를 사용합니다. 각 필드는 독립적으로 범위가 지정되므로 필드 정의 순서를 변경해도 추출에는 영향을 미치지 않습니다.

// Each field is independent — reorder freely without breaking extraction
var invoiceNumber = ReadRegion(imagePath, 450, 80,  200, 30);
var invoiceDate   = ReadRegion(imagePath, 450, 115, 200, 30);
var vendorName    = ReadRegion(imagePath, 50,  150, 300, 40);
var totalAmount   = ReadRegion(imagePath, 450, 600, 200, 30);
// Each field is independent — reorder freely without breaking extraction
var invoiceNumber = ReadRegion(imagePath, 450, 80,  200, 30);
var invoiceDate   = ReadRegion(imagePath, 450, 115, 200, 30);
var vendorName    = ReadRegion(imagePath, 50,  150, 300, 40);
var totalAmount   = ReadRegion(imagePath, 450, 600, 200, 30);
' Each field is independent — reorder freely without breaking extraction
Dim invoiceNumber = ReadRegion(imagePath, 450, 80, 200, 30)
Dim invoiceDate = ReadRegion(imagePath, 450, 115, 200, 30)
Dim vendorName = ReadRegion(imagePath, 50, 150, 300, 40)
Dim totalAmount = ReadRegion(imagePath, 450, 600, 200, 30)
$vbLabelText   $csharpLabel

리드툴즈 OCR 마이그레이션 체크리스트

사전 마이그레이션

변경 작업을 수행하기 전에 코드베이스를 감사하여 LEADTOOLS 사용 부분을 모두 확인하십시오.

# Find all LEADTOOLS namespace imports
grep -rn "using Leadtools" --include="*.cs" .

# Find engine lifecycle calls
grep -rn "OcrEngineManager\|\.Startup(\|\.Shutdown()" --include="*.cs" .

# Find license file references
grep -rn "SetLicense\|LEADTOOLS\.LIC\|\.LIC\.KEY" --include="*.cs" .

# Find RasterCodecs usage
grep -rn "RasterCodecs\|_codecs\.Load\|GetInformation" --include="*.cs" .

# Find DocumentWriter usage
grep -rn "DocumentWriterInstance\|PdfDocumentOptions\|DocumentFormat\." --include="*.cs" .

# Find zone-based OCR
grep -rn "OcrZone\|page\.Zones\|LeadRect\|ZoneType" --include="*.cs" .

# Find GC workarounds to remove
grep -rn "GC\.Collect\|WaitForPendingFinalizers" --include="*.cs" .
# Find all LEADTOOLS namespace imports
grep -rn "using Leadtools" --include="*.cs" .

# Find engine lifecycle calls
grep -rn "OcrEngineManager\|\.Startup(\|\.Shutdown()" --include="*.cs" .

# Find license file references
grep -rn "SetLicense\|LEADTOOLS\.LIC\|\.LIC\.KEY" --include="*.cs" .

# Find RasterCodecs usage
grep -rn "RasterCodecs\|_codecs\.Load\|GetInformation" --include="*.cs" .

# Find DocumentWriter usage
grep -rn "DocumentWriterInstance\|PdfDocumentOptions\|DocumentFormat\." --include="*.cs" .

# Find zone-based OCR
grep -rn "OcrZone\|page\.Zones\|LeadRect\|ZoneType" --include="*.cs" .

# Find GC workarounds to remove
grep -rn "GC\.Collect\|WaitForPendingFinalizers" --include="*.cs" .
SHELL

마이그레이션 후 검증 과정에서 동일한IronOCR기능이 테스트되도록 구매한 LEADTOOLS 번들(OCR 모듈, PDF 모듈 및 엔진 유형(LEAD, Tesseract 또는 OmniPage))을 기록해 두십시오.

코드 마이그레이션

  1. 모든 LEADTOOLS NuGet 패키지를 제거하십시오: Leadtools, Leadtools.Ocr, Leadtools.Codecs, Leadtools.Forms.DocumentWriters, Leadtools.Pdf
  2. IronOcr NuGet Install-Package
  3. 모든 LEADTOOLS using 지시어를 using IronOcr;로 교체하십시오
  4. 프로젝트 및 배포 아티팩트에서 .LIC.LIC.KEY 파일 제거
  5. 애플리케이션 시작 시 RasterSupport.SetLicense(licPath, keyContent)IronOcr.License.LicenseKey = "key"로 교체하십시오
  6. 전적으로 IOcrEngineRasterCodecs 수명 관리를 위해 존재하는 IDisposable 서비스 클래스를 모두 삭제하십시오
  7. OcrEngineManager.CreateEngine() + engine.Startup()new IronTesseract()으로 교체하십시오
  8. _codecs.Load(imagePath)using var input = new OcrInput() 블록 내의 input.LoadImage(imagePath)로 교체하십시오
  9. 다중 프레임 TIFF 페이지 루프를 input.LoadImageFrames(tiffPath)로 교체하십시오
  10. PDF 페이지 반복 루프를 input.LoadPdf(pdfPath)로 교체하십시오
  11. document.Pages.AddPage() + page.Recognize(null) + page.GetText(-1)ocr.Read(input).Text로 교체하십시오
  12. OcrZone + page.Zones.Add() 패턴을 input.LoadImage(path, new CropRectangle(x, y, w, h))로 교체하십시오
  13. PdfDocumentOptions + DocumentWriterInstance.SetOptions() + document.Save()result.SaveAsSearchablePdf(path)로 교체하십시오
  14. OcrInput 인스턴스에서 input.Deskew(), input.DeNoise(), input.Binarize()로 전처리 명령 클래스를 (DeskewCommand, DespeckleCommand, AutoBinarizeCommand) 교체하십시오
  15. LEADTOOLS 메모리 관리를 보상하기 위해 추가된 모든 GC.Collect() / GC.WaitForPendingFinalizers() 호출 제거

마이그레이션 이후

  • 대표적인 이미지 및 PDF 샘플에서 인식된 텍스트 출력이 LEADTOOLS 출력과 일치하는지 확인합니다.
  • result.Confidence을 사용하여 신뢰도 점수가 예상 범위 내에 있는지 확인하십시오
  • 다중 프레임 TIFF 처리 테스트에서 LEADTOOLS 프레임 반복 루프와 동일한 페이지 수가 생성되는지 확인합니다.
  • 검색 가능한 PDF 출력물이 PDF 리더(Adobe Acrobat 또는 이와 동등한 프로그램)에서 텍스트 검색이 가능한지 확인합니다.
  • 생산 송장 또는 양식의 정상 필드 값과 비교하여 영역 기반 필드 추출을 테스트합니다.
  • 별도의 Leadtools.Pdf 모듈 없이 비밀번호로 보호된 PDF 복호화가 작동하는지 확인하십시오
  • 모든 GC.Collect()를 먼저 제거한 후 부하 상태에서 배치 처리를 실행하고 메모리 성장이 없음을 확인하십시오
  • 라이선스 파일 없이 Docker 및 CI/CD 배포를 테스트하고, 환경 변수에서 "라이선스 키" 문자열이 올바르게 해석되는지 확인합니다.
  • Parallel.ForEach와 함께 병렬 처리를 테스트하여 스레드 안전성을 확인하십시오
  • 구조화된 데이터 추출 (result.Pages, page.Paragraphs, page.Words)이 올바른 좌표를 반환하는지 확인하십시오

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

배포는 상태 없는 것이 됩니다. LEADTOOLS .LIC.LIC.KEY 파일은 모든 배포 환경에서 지니고 있어야 하는 아티팩트입니다. 라이선스 데이터를 이미지 기록에 노출하는 컨테이너는 라이선스 정보를 이미지에 포함시킵니다. 이를 장착하는 컨테이너는 부피 조정이 필요합니다.IronOCR로 마이그레이션한 후에는 라이선스가 환경 변수에 문자열로 저장됩니다. 배포 아티팩트는 표준 NuGet 참조입니다. 파일도 없고, 경로도 없고, 마운트 전략도 없습니다. Docker 배포 가이드Azure 배포 가이드는 컨테이너화된 환경을 위한 전체 설정 과정을 보여줍니다.

네 개의 패키지가 하나로 됩니다. 마이그레이션은 Leadtools, Leadtools.Ocr, Leadtools.Codecs, Leadtools.Forms.DocumentWriters, 옵션으로 Leadtools.Pdf를 단일 IronOcr 참조로 줄입니다. 전처리, 네이티브 PDF 입력, 검색 가능한 PDF 출력, 바코드 판독, 구조화된 데이터 추출, 125개 이상의 언어 지원 등 모든 기능이 하나의 패키지에 포함되어 있습니다. 기능 구성은 어떤 패키지를 구매했는지에 따라 달라지지 않습니다.

배치 처리는 수동 메모리 관리를 제거합니다. LEADTOOLS 배치 코드는 다중 문서 실행 중에 메모리 누적을 방지하기 위한 방어적인 GC.Collect() 호출 및 명시적인 RasterImage 폐기를 포함합니다. IronOCR의 OcrInputusing 블록과 범위가 지정되어 자동으로 정리를 처리합니다. 스레드 안전한 병렬 처리는 Parallel.ForEach로 수행됩니다—스레드당 하나의 IronTesseract 인스턴스—싱크로나이제이션 코드 없이 멀티코어 쓰루풋을 제공합니다. 생산 처리량 조정을 위한 속도 최적화 가이드를 참조하십시오.

예측 가능한 총비용. LEADTOOLS는 5인 개발자 팀 기준으로 첫 해 예상 비용이 15,000달러에서 40,000달러이며, 업데이트를 받으려면 라이선스 비용의 약 20~25%에 해당하는 연간 유지 보수 비용이 필요합니다.IronOCR Professional 2,999달러이며, 10명의 개발자와 10개의 배포 위치를 지원하는 일회성 영구 구매 상품입니다. 1년간의 업데이트가 포함되어 있습니다. 업데이트 기간 이후 계속 사용하시는 경우 추가 비용은 발생하지 않습니다. IronOCR 라이선스 페이지에는 영업 상담 없이 모든 등급별 가격이 직접 게시되어 있습니다.

영역 설정 없는 구조화된 데이터입니다. 마이그레이션 후, 단어, 라인, 및 문단 수준의 데이터가 OcrResult에서 직접 경계 상자 좌표와 함께 사용 가능합니다—영역 정의가 필요하지 않습니다. 다섯 단계의 계층 구조 (Pages, Paragraphs, Lines, Words, Characters) 각각이 위치 좌표와 요소별 신뢰도 점수를 표시합니다. 구조화된 데이터 추출을 위해 LEADTOOLS 영역 구성이 필요했던 애플리케이션은 더 간단한 API를 통해 더욱 풍부한 데이터를 얻을 수 있습니다. OCR 결과 기능 페이지에는 전체 출력 모델이 요약되어 있습니다.

참고해 주세요Adobe Acrobat, Kofax OmniPage, LEADTOOLS 및 Tesseract는 각각 소유자의 등록 상표입니다. 이 사이트는 Adobe Inc., Apryse, Google, Kofax 또는 LEAD Technologies와 제휴, 지지, 또는 후원을 받지 않았습니다. 모든 제품명, 로고 및 브랜드는 해당 소유자의 자산입니다. 비교는 정보 제공 목적으로만 사용되며, 작성 시점에 공개적으로 이용 가능한 정보를 반영합니다.

자주 묻는 질문

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

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

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

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

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

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

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

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

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

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

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

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

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

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

IronOCR이 리드툴즈 OCR과 동일한 방식으로 PDF를 처리할 수 있나요?

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

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

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

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

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

IronOCR 가격이 워크로드 확장을 위해 LEADTOOLS OCR보다 더 예측 가능한가요?

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

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

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

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

아이언 서포트 팀

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