Jak przeprowadzić OCR w NET Maui
Niniejszy przewodnik przeprowadza programistów .NET przez proces pełnej migracji zLEADTOOLS OCRdo IronOCR. Obejmuje on każdy etap, od wymiany pakietu NuGet po pełną migrację kodu, wraz z przykładami "przed i po" dla wzorców najbardziej dotkniętych procedurą inicjalizacji LEADTOOLS, strukturą wielu przestrzeni nazw oraz modelem wdrażania licencji opartym na plikach.
Dlaczego warto przejść z LEADTOOLS OCR
LEADTOOLS OCR jest dostarczany w ramach korporacyjnej platformy obrazówania, której korzenie sięgają 1990 roku, a API odzwierciedla tę tradycję. Minimalna konfiguracja robocza wymaga czterech pakietów NuGet, czterech przestrzeni nazw, dwóch fizycznych plików licencji wdrożonych w znanej ścieżce, warstwy kodeków zainicjowanej przed silnikiem, wyboru typu silnika spośród trzech opcji oraz blokującego wywołania startowego, które ładuje pliki binarne środowiska uruchomieniowego do pamięci. Wszystko to dzieje się, zanim rozpoznany zostanie choćby jeden znak. Zespoły przechodzące na IronOCR eliminują całą tę warstwę — jeden pakiet, jedna przestrzeń nazw, jedna linijka kodu do ustawienia klucza licencyjnego.
Oparty na plikach rozlokowanie licencji ulega przerwaniu w kontenerach. LEADTOOLS wymaga dwóch fizycznych plików — LEADTOOLS.LIC i LEADTOOLS.LIC.KEY — dostępnych do odczytu w określonej ścieżce na każdej maszynie uruchamiającej aplikację. W kontenerach Docker pliki te muszą być albo wbudowane w obraz (co powoduje ich ujawnienie w historii warstw), albo zamontowane w czasie wykonywania (co wymaga koordynacji woluminów w każdym środowisku orkiestracji). Azure Functions i AWS Lambda nie posiadają mechanizmu wdrażania plików licencyjnych bez stosowania rozwiązań alternatywnych. Pipeline'y CI/CD wymagają, aby pliki znajdowały się dokładnie w ścieżce używanej przez wywołanie startowe, w przeciwnym razie aplikacja zgłosi błąd przed przetworzeniem jakiegokolwiek dokumentu.IronOCR zastępuje oba pliki ciągiem znaków, który mieści się w zmiennej środowiskowej, sekrecie Kubernetes lub odwołaniu do Azure Key Vault.
Cztery pakiety dla jednego zadania. Działający projekt OCR LEADTOOLS wymaga co najmniej Leadtools, Leadtools.Ocr, Leadtools.Codecs i Leadtools.Forms.DocumentWriters. Obsługa PDF zabezpieczonych hasłem wymaga dodatkowego modułu Leadtools.Pdf, który może nie być zawarty w zakupionej paczce. Każdy pakiet musi być obecny, kompatybilny i posiadać wspólną wersję.IronOCR jest dostarczany jako jeden pakiet NuGet. Uwzględniono wszystkie funkcje — natywne wprowadzanie plików PDF, generowanie plików PDF z możliwością wyszukiwania, przetwarzanie wstępne, odczyt kodów BarCode.
Cykl życia silnika tworzy powierzchnię konserwacyjną. LEADTOOLS otacza silnik OCR w klasie serwisowej IDisposable, nie dla idiomatycznych powodów .NET, lecz ponieważ wywołanie Shutdown() musi poprzedzać Dispose(), inaczej aplikacja wywołuje błędy. Produkcyjna implementacja przetwarzaczy wsadowych LEADTOOLS zazwyczaj obejmuje wywołania GC.Collect() / GC.WaitForPendingFinalizers() pomiędzy częściami dokumentów, aby zrekompensować gromadzenie się instancji RasterImage.IronOCR używa standardowych bloków using. OcrInput to jedyny obiekt wymagający zlikwidowania.
W środowisku produkcyjnym pojawiają się problemy z pakietami. Firma LEADTOOLS nie publikuje cen. Pakiet Document Imaging SDK — poziom obejmujący OCR — kosztuje szacunkowo 3000–8000 USD rocznie na programistę. Zespoły, które zakupiły moduł OCR bez modułu PDF, odkrywają lukę, gdy RasterSupport.IsLocked() zwraca true w odniesieniu do dokumentów zabezpieczonych hasłem w produkcji. Poziom dokładności silnika OmniPage wymaga oddzielnej umowy licencyjnej Kofax oprócz zakupu LEADTOOLS.IronOCR udziela licencji na wszystkie funkcje na każdym poziomie. Nie ma żadnych powiązań z dostawcami zewnętrznymi, nie ma też oddzielnego modułu do obsługi dokumentów zaszyfrowanych.
Koszt inicjalizacji wpływa na zimne starty. engine.Startup() to blokujące wywołanie, które ładuje pliki wykonawcze do pamięci. W środowiskach bezserwerowych, w których liczy się opóźnienie przy zimnym starcie — Azure Functions, AWS Lambda — blokująca inicjalizacja trwająca 500–2000 ms przed rozpoczęciem jakichkolwiek działań rozpoznawczych stanowi problem strukturalny.IronOCR wykorzystuje leniwą inicjalizację. Instancja IronTesseract inicjalizuje się przy pierwszym użyciu, a kolejne wywołania w tym samym procesie nie ponoszą żadnego kosztu inicjalizacji.
Podstawowy problem
LEADTOOLS wymaga czterech przestrzeni nazw, czterech pakietów NuGet oraz obowiązkowej procedury uruchamiania przed rozpoznaniem znaku:
// 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
// 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
IronOCR a LEADTOOLS OCR: porównanie funkcji
Poniższa tabela przedstawia bezpośrednie powiązania między funkcjami obu bibliotek.
| Funkcja | LEADTOOLS OCR | IronOCR |
|---|---|---|
| Wymagane pakiety NuGet | Minimum 4 strony (więcej w przypadku pliku PDF) | 1 (IronOcr) |
| Mechanizm licencji | .LIC + .LIC.KEY para plików |
Klucz ciągu znaków |
| Wdrażanie licencji | Pliki na każdym komputerze produkcyjnym | Zmienna środowiskowa lub konfiguracja |
| Model cenowy | 3 000–15 000 USD+/programista/rok (szacunkowo) | $999–2 999 USD jednorazowy |
| Inicjalizacja silnika | Ręczne Startup() z ścieżką wykonawczą |
Automatyczne, leniwe |
| Wyłączenie silnika | Ręczne Shutdown() przed Dispose() |
Nie jest wymagane |
| Warstwa kodeków | RasterCodecs wymagane dla wszystkich ładowań obrazów |
Nie jest wymagane |
| Plik wejściowy PDF | Pętla rasteryzacji strona po stronie | Wbudowany parametr Password |
| Plik PDF chroniony hasłem | Wymaga oddzielnego modułu Leadtools.Pdf |
Wbudowany parametr Password |
| Wynik w formacie PDF z możliwością wyszukiwania | DocumentWriter + PdfDocumentOptions + document.Save() |
result.SaveAsSearchablePdf() |
| Przetwarzanie wstępne | Oddzielne klasy komend (DeskewCommand, DespeckleCommand, itd.) |
Wbudowane metody filtracji na OcrInput |
| Wielostronicowy plik TIFF | Ręczna iteracja klatek przy użyciu CodecsLoadByteOrder |
input.LoadImageFrames() |
| Strukturalny wynik | Poziom strony i strefy | Strona, akapit, wiersz, słowo, znak z współrzędnymi |
| Ocena pewności | OcrPageRecognizeStatus enum |
result.Confidence jako procent |
| Odczytywanie BarCode | Oddzielny moduł BarCode LEADTOOLS | Wbudowany (ReadBarCodes = true) |
| Bezpieczeństwo wątków | Wymaga starannego zarządzania | Pełny (jeden IronTesseract na wątek) |
| Obsługiwane języki | 60–120 (w zależności od silnika) | Ponad 125 pakietów językowych dostępnych za pośrednictwem NuGet |
| Wdrożenie językowe | tessdata files or engine-bundled files | Pakiet NuGet dla każdego języka |
| Wielopłatformowe | Obsługa konfiguracji środowiska uruchomieniowego dla poszczególnych platform | Windows, Linux, macOS, Docker, Azure, AWS |
| Wdrożenie Docker | Pliki .KEY muszą być zamontowane lub dodane do obrazu |
Standard dotnet publish |
| Wsparcie komercyjne | Tak | Tak |
Szybki start: Migracja zLEADTOOLS OCRdo IronOCR
Krok 1: Zastąp pakiet NuGet
Usuń wszystkie pakiety 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
Zainstaluj IronOCR z NuGet:
dotnet add package IronOcr
Krok 2: Aktualizacja przestrzeni nazw
Zastąp wszystkie dyrektywy LEADTOOLS using jednym importem 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
Krok 3: Inicjalizacja licencji
Usuń wszystkie wywołania RasterSupport.SetLicense() oraz referencje do plików .LIC / .LIC.KEY. Dodaj klucz licencyjny IronOCR podczas uruchamiania aplikacji:
// 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")
Klucz licencyjny może być przechowywany w dowolnym standardowym wzorcu zarządzania sekretami .NET — appsettings.json, Azure Key Vault, AWS Secrets Manager lub sekret Kubernetes. Nie ma potrzeby wdrażania żadnych plików wraz z plikiem binarnym aplikacji.
Przykłady migracji kodu
Uruchamianie i wyłączanie silnika Usunięcie cyklu życia
LEADTOOLS wymaga ścisłego przestrzegania wzorca klasy usługowej, ponieważ cykl życia silnika musi być zarządzany w sposób jawny. Konstruktor uruchamia silnik, Dispose() wyłącza go w odpowiedniej kolejności, a każdy kod, który pomija Shutdown() przed Dispose(), wywołuje błąd wykonania.
Podejście 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
Podejście 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
Instancja IronTesseract inicjalizuje się przy pierwszym użyciu. Brak argumentów konstruktora, brak ścieżki wykonawczej, brak wywołania Startup(). Powyższa klasa serwisowa w ogóle nie musi implementować IDisposable — silnik jest bezstanowy, a obiekty OcrInput używane wewnątrz każdego wywołania Read() zajmują się własnym czyszczeniem przez standardowy wzorzec using. Podręcznik konfiguracji IronTesseract obejmuje opcje konfiguracyjne, w tym wybór języka i optymalizację wydajności dla scenariuszy produkcyjnych.
Wieloklatkowe przetwarzanie partii plików TIFF
LEADTOOLS przetwarza pliki TIFF z wieloma ramkami, pobierając wszystkie ramki z kodera, a następnie iterując z explicytnymi parametrami lastPage przy każdym wywołaniu _codecs.Load(). Każdy obraz ramki musi zostać usunięty ręcznie, w przeciwnym razie pamięć będzie się zapełniać.
Podejście 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
Podejście 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
LoadImageFrames() odczytuje wszystkie ramki z TIFFu w jednym wywołaniu. Bez zapytań o liczbę klatek, bez pętli, bez jawnego usuwania po każdej klatce. Wersja równoległa tworzy jedną instancję IronTesseract na wątek, co jest prawidłowym wzorcem — patrz przykład wielowątkowości dla pełnego modelu wątków. W przypadku opcji wejściowych specyficznych dla formatu TIFF, przewodnik dotyczący formatów TIFF i GIF obejmuje wybór klatek oraz obsługę wielu formatów.
Uproszczenie procesu tworzenia dokumentacji
Tworzenie PDFów z możliwością wyszukiwania w LEADTOOLS wymaga skonfigurowania instancji DocumentWriter na silniku, skonstruowania obiektu PdfDocumentOptions z typem wyjściowym i ustawieniami nakładki, zastosowania opcji przez SetOptions(), a następnie wywołania document.Save() z formatem enum. Każdy z tych kroków jest oddzielnym obiektem i oddzielnym wywołaniem API.
Podejście 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
Podejście 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
SaveAsSearchablePdf() zastępuje cały łańcuch PdfDocumentOptions + SetOptions() + document.Save(). Warstwa obrazu nakładająca się na tekst działa automatycznie. Aby uzyskać pełną dokumentację dotyczącą plików PDF z funkcją wyszukiwania, zapoznaj się z przewodnikiem dotyczącym plików PDF z funkcją wyszukiwania, który omawia opcje wyjściowe, oraz przykładem pliku PDF z funkcją wyszukiwania, który pokazuje integrację ze strumieniami odpowiedzi ASP.NET.
Migracja ekstrakcji pól wielostrefowych
OCR oparte na strefach w LEADTOOLS używa obiektów OcrZone z granicami LeadRect, atrybutami OcrZoneType i właściwościami OcrZoneCharacterFilters. Wiele stref jest dodawanych do jednej strony i rozpoznawanych w jednym wywołaniu page.Recognize(), a następnie ekstraktowane według indeksu stref. Indeks stref odpowiada kolejności wstawiania, co oznacza, że pętla wyodrębniania musi zachować tę kolejność.
Podejście 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
Podejście 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
CropRectangle przekazywane bezpośrednio do LoadImage() zastępuje całą konfigurację OcrZone. Nie ma potrzeby śledzenia indeksu strefy, nie jest wymagane wywołanie page.Zones.Clear(), ani nie jest potrzebna kontrola statusu rozpoznania. Przewodnik po OCR oparty na regionach obejmuje wzorce ekstrakcji jedno- i wieloregionalnej. Aby zapoznać się z kompletnym samouczkiem dotyczącym wyodrębniania pól z faktur, zobacz samouczek dotyczący OCR faktur.
Pobieranie danych ustrukturyzowanych za pomocą współrzędnych słów WORD
Strukturalne dane wyjściowe LEADTOOLS działają na poziomie strony i strefy. Aby uzyskać dane na poziomie słów z współrzędnymi ramki ograniczającej, programista uzyskuje dostęp do obiektów OcrWord wewnątrz rozpoznawanej strefy. API wymaga przetworzenia kolekcji stref po rozpoznaniu i iteracji listy słów dla każdej strefy.
Podejście 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
Podejście 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
Hierarchia wyników IronOCR schodzi od Pages przez Paragraphs, Lines, Words i Characters. Każdy poziom udostępnia X, Y, Width, Height, Text i Confidence. Wzorzec dostępu oparty na strefach LEADTOOLS znika — nie jest wymagana iteracja stref, aby uzyskać dostęp do danych WORD. Przewodnik "Wyniki odczytu" obejmuje kompletny model wyjściowy wraz ze schematami dostępu do współrzędnych. Dokumentacja API OcrResult opisuje każdą właściwość w hierarchii wyników.
Dokumentacja APILEADTOOLS OCRdo IronOCR
| LEADTOOLS OCR | IronOCR |
|---|---|
RasterSupport.SetLicense(licPath, keyContent) |
IronOcr.License.LicenseKey = "key" |
new RasterCodecs() |
Nie jest wymagane |
OcrEngineManager.CreateEngine(OcrEngineType.LEAD) |
new IronTesseract() |
engine.Startup(codecs, null, null, runtimePath) |
Nie jest wymagane |
engine.IsStarted |
Nie jest wymagane (zawsze gotowe) |
engine.Shutdown() |
Nie jest wymagane |
engine.Dispose() |
Nie jest wymagane |
_codecs.Load(imagePath) |
input.LoadImage(imagePath) |
_codecs.Load(path, 0, BgrOrGray, page, page) |
input.LoadPdf(path) lub input.LoadImageFrames(path) |
_codecs.GetInformation(path, true).TotalPages |
Nie jest wymagane — automatyczne |
engine.DocumentManager.CreateDocument() |
Nie jest wymagane |
document.Pages.AddPage(image, null) |
input.LoadImage(imagePath) |
page.Recognize(null) |
ocr.Read(input) (rozpoznanie jest częścią Read()) |
page.GetText(-1) |
result.Text |
page.RecognizeStatus |
result.Confidence (procent) |
OcrZone { Bounds = new LeadRect(x, y, w, h) } |
new CropRectangle(x, y, w, h) |
page.Zones.Clear() |
Nie jest wymagane |
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 } |
Obsługiwane automatycznie przez SaveAsSearchablePdf() |
engine.DocumentWriterInstance.SetOptions(format, opts) |
Nie jest wymagane |
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 |
Typowe problemy związane z migracją i ich rozwiązania
Problem 1: Błędy rozpoznawania ścieżki do pliku licencji
LEADTOOLS: RasterSupport.SetLicense() rozwiązuje ścieżki plików .LIC i .LIC.KEY względem katalogu roboczego, co różni się w bin/Debug, bin/Release, kontenerach Docker i pulach aplikacji IIS. Typowym trybem awarii jest ścieżka, która działa podczas rozwoju, ale wywołuje "License file not found" w produkcji, ponieważ katalog roboczy uległ zmianie.
Rozwiązanie: Usuń oba pliki licencyjne i całkowicie usuń wywołanie SetLicense(). Zastąp pojedynczym przypisaniem ciągu znaków, które odczytuje wartość ze zmiennej środowiskowej:
// 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"))
Klucz ciągu znaków zachowuje się identycznie w każdym środowisku. Zapisz to w tym samym menedżerze sekretów, który jest już używany dla ciągów połączeń z bazą danych.
Problem 2: Wyjątek "Silnik nie został uruchomiony"
LEADTOOLS: Wywołanie dowolnej metody rozpoznania przed ukończeniem engine.Startup(), lub po wywołaniu engine.Shutdown() (na przykład w sytuacji wyścigu wymuszania usunięcia przed użyciem podczas zamykania), wywołuje InvalidOperationException z "Silnik nie został uruchomiony." Trwałe klasy serwisowe muszą się przed tym zabezpieczyć przy pomocy IsStarted.
Rozwiązanie: IronTesseract nie wymaga wywołania startowego i nie ma statusu rozpoczęcia/zatrzymania. Blokada IsStarted i cała klasa serwisowa cyklu życia mogą zostać usunięte:
// 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);
Problem 3: Gromadzenie pamięci przez RasterImage podczas przetwarzania wsadowego
LEADTOOLS: Przetwarzanie wsadowe iterujące po stronach PDF lub plikach obrazów tworzy instancje RasterImage w pętli. Jeśli jakakolwiek ścieżka kodu nie zlikwiduje RasterImage — z powodu wyjątku wyrzuconego przed zakończeniem bloku using, lub przekazywania manualnego z usuniętym wywołaniem — niezlikwidowane obrazy gromadzą się w pamięci. Kod produkcyjny LEADTOOLS zazwyczaj obejmuje wywołania GC.Collect() / GC.WaitForPendingFinalizers() między wsadami jako mechanizm kompensujący.
Rozwiązanie: Usuń wszystkie wywołania GC.Collect(). OcrInput to jedyny IDisposable w potoku IronOCR, i jest przypisany do każdej operacji wsadowej ze standardowym blokiem 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
Aby uzyskać dodatkowe wskazówki dotyczące optymalizacji pamięci, zapoznaj się z wpisem na blogu poświęconym redukcji alokacji pamięci.
Problem 4: Opcje DocumentWriterInstance są zachowywane między wywołaniami
LEADTOOLS: engine.DocumentWriterInstance.SetOptions() modyfikuje stan na współdzielonej instancji piszącej silnika. Jeżeli jedna ze ścieżek kodu ustawi PdfDocumentOptions z DocumentType = PdfDocumentType.PdfA, a kolejne wywołanie nie zresetuje tych opcji przed wywołaniem document.Save(), format wyjściowy z poprzedniego wywołania zostaje utrzymany. Jest to efekt uboczny z zachowaniem stanu w współdzielonej instancji silnika.
Rozwiązanie:IronOCR nie posiada współdzielonego stanu pisania. Każde wywołanie SaveAsSearchablePdf() jest niezależne:
// 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)
Każde wywołanie generuje standardowy plik PDF z nakładką graficzną i warstwą tekstową z możliwością wyszukiwania. Nie ma wspólnego stanu opcji, który można by zresetować między wywołaniami.
Problem 5: Nieprawidłowy pakiet — brakujący moduł w czasie wykonywania
LEADTOOLS: Zespoły, które zakupiły SDK obrazowania dokumentów, mogą stwierdzić, że typy Leadtools.Pdf są niedostępne, lub że zaszyfrowane strony PDF wyrzucają RasterException z RasterExceptionCode.FeatureNotSupported. Dzieje się tak, gdy zakupiony pakiet nie zawiera modułu PDF, a błąd ujawnia się dopiero w czasie wykonywania w środowisku produkcyjnym.
Rozwiązanie:IronOCR zawiera wszystkie funkcje na każdym poziomie licencji. Nie ma oddzielnego modułu PDF, dodatku dla zaszyfrowanych dokumentów ani drugiego dostawcy dla silnika z wyższą dokładnością. Po zainstalowaniu pojedynczego pakietu IronOcr, pełen zestaw funkcji jest dostępny bez żadnych dodatkowych zakupów:
dotnet add package IronOcr
Problem 6: Niezgodność indeksu stref po zmianie kolejności
LEADTOOLS: Ekstrakcja stref używa indeksowania pozycyjnego — page.Zones[0].Text, page.Zones[1].Text — co wiąże logikę ekstrakcji z kolejnością wstawiania w page.Zones.Add(). Zmiana kolejności definicji stref w celu dopasowania do zmienionego układu formularza powoduje ciche przerwanie ekstrakcji poprzez przesunięcie wszystkich kolejnych indeksów.
Rozwiązanie:IronOCR używa nazwanych zmiennych z CropRectangle na pole. Zmiana kolejności definicji pól nie ma wpływu na ekstrakcję, ponieważ zakres każdego pola jest niezależny:
// 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)
Lista kontrolna migracji LEADTOOLS OCR
Przed migracją
Przed wprowadzeniem jakichkolwiek zmian należy przeprowadzić audyt kodu źródłowego w celu zidentyfikowania wszystkich miejsc użycia 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" .
Należy odnotować, który pakiet LEADTOOLS został zakupiony — moduł OCR, moduł PDF oraz typ silnika (LEAD vs Tesseract vs OmniPage) — aby zapewnić przetestowanie równoważnych funkcji IronOCR podczas walidacji po migracji.
Migracja kodu
- Usuń wszystkie pakiety NuGet LEADTOOLS:
Leadtools,Leadtools.Ocr,Leadtools.Codecs,Leadtools.Forms.DocumentWriters,Leadtools.Pdf - Zainstaluj pakiet NuGet
IronOcr - Zastąp wszystkie dyrektywy LEADTOOLS
usingzusing IronOcr; - Usuń z projektu i artefaktów wdrożeniowych pliki
.LICi.LIC.KEY - Zastąp
RasterSupport.SetLicense(licPath, keyContent)zIronOcr.License.LicenseKey = "key"podczas uruchamiania aplikacji - Usuń wszystkie klasy serwisowe
IDisposable, które istnieją wyłącznie do zarządzania cyklem życiaIOcrEngineiRasterCodecs - Zastąp
OcrEngineManager.CreateEngine()+engine.Startup()znew IronTesseract() - Zastąp
_codecs.Load(imagePath)zinput.LoadImage(imagePath)wewnątrz blokuusing var input = new OcrInput() - Zastąp pętle stron TIFF z wieloma ramkami używając
input.LoadImageFrames(tiffPath) - Zastąp pętlę iteracji po stronach PDF używając
input.LoadPdf(pdfPath) - Zastąp
document.Pages.AddPage()+page.Recognize(null)+page.GetText(-1)zocr.Read(input).Text - Zastąp wzorce
OcrZone+page.Zones.Add()zinput.LoadImage(path, new CropRectangle(x, y, w, h)) - Zastąp
PdfDocumentOptions+DocumentWriterInstance.SetOptions()+document.Save()zresult.SaveAsSearchablePdf(path) - Zastąp klasy komend przetwarzania wstępnego (
DeskewCommand,DespeckleCommand,AutoBinarizeCommand) zinput.Deskew(),input.DeNoise(),input.Binarize()na instancjiOcrInput - Usuń wszystkie wywołania
GC.Collect()/GC.WaitForPendingFinalizers()dodane w celu rekompensaty zarządzania pamięcią LEADTOOLS
Po migracji
- Sprawdź, czy rozpoznany tekst wyjściowy odpowiada wynikom LEADTOOLS na reprezentatywnej próbce obrazów i plików PDF
- Potwierdź, że wskaźniki zaufania mieszczą się w oczekiwanych zakresach, używając
result.Confidence - Test przetwarzania plików TIFF z wieloma ramkami daje taką samą liczbę stron jak pętla iteracyjna ramek w LEADTOOLS
- Sprawdź, czy plik PDF z możliwością wyszukiwania umożliwia wyszukiwanie tekstu w czytniku PDF (Adobe Acrobat lub jego odpowiednik)
- Przetestuj ekstrakcję pól opartą na strefach w odniesieniu do sprawdzonych wartości pól z faktur lub formularzy produkcyjnych
- Zweryfikuj, że rozszyfrowywanie PDF zabezpieczonych hasłem działa bez oddzielnego modułu
Leadtools.Pdf - Uruchom przetwarzanie wsadowe pod obciążeniem i potwierdź brak wzrostu pamięci (najpierw usuń wszystkie
GC.Collect()) - Przetestuj wdrożenie Docker i CI/CD bez plików licencyjnych — sprawdź, czy klucz licencyjny typu string jest poprawnie rozpoznawany ze zmiennej środowiskowej
- Przetestuj przetwarzanie równoległe z użyciem
Parallel.ForEach, aby zweryfikować bezpieczeństwo wątków - Potwierdź, że ekstrakcja strukturalna danych (
result.Pages,page.Paragraphs,page.Words) zwraca poprawne współrzędne
Kluczowe korzyści z migracji do IronOCR
Rozlokowanie staje się bezstanowe. Pliki LEADTOOLS .LIC i .LIC.KEY są artefaktami, które każde środowisko wdrożeniowe musi posiadać. Kontenery, w których są one wbudowane, ujawniają dane licencyjne w historii obrazów. Kontejnery, które je montują, wymagają koordynacji woluminów. Po migracji do IronOCR licencja jest ciągiem znaków w zmiennej środowiskowej. Artefaktem wdrożeniowym jest standardowe odwołanie NuGet. Żadnych plików, żadnych ścieżek, żadnej strategii montowania. Przewodnik wdrażania Docker oraz przewodnik wdrażania Azure przedstawiają kompletną konfigurację środowisk kontenerowych.
Cztery pakiety stają się jednym. Migracja redukuje Leadtools, Leadtools.Ocr, Leadtools.Codecs, Leadtools.Forms.DocumentWriters, i opcjonalnie Leadtools.Pdf do pojedynczej referencji IronOcr. Wszystkie funkcje — przetwarzanie wstępne, natywne dane wejściowe w formacie PDF, dane wyjściowe w formacie PDF z możliwością wyszukiwania, odczyt kodów BarCode, ekstrakcja danych strukturalnych, obsługa ponad 125 języków — są zawarte w tym jednym pakiecie. Zestaw funkcji nie zależy od tego, który pakiet został zakupiony.
Przetwarzanie wsadowe eliminuje ręczne zarządzanie pamięcią. Kod wsadowy LEADTOOLS zawiera defensywne wywołania GC.Collect() i explicytne zlikwidowanie RasterImage, aby zapobiec nagromadzeniu pamięci podczas uruchamiania wielu dokumentów. Zakres OcrInputIronOCR z blokiem using zarządza sprzątaniem automatycznie. Bezpieczne dla wątków przetwarzanie równoległe z Parallel.ForEach — jedna instancja IronTesseract na wątek — dostarcza przepustowość wielordzeniową bez kodu synchronizującego. Zapoznaj się z przewodnikiem po optymalizacji prędkości, aby dostroić przepustowość produkcji.
Przewidywalny całkowity koszt. Szacowany koszt LEADTOOLS dla pięcioosobowego zespołu programistów wynosi 15 000–40 000 USD w pierwszym roku, a roczna opłata za utrzymanie, wymagana do otrzymywania aktualizacji, wynosi około 20–25% kosztu licencji.IronOCR Professional w cenie 2999 USD obejmuje dziesięciu programistów i dziesięć lokalizacji wdrożeniowych w ramach jednorazowego zakupu na czas nieograniczony. W cenie zawarty jest roczny dostęp do aktualizacji. Dalsze korzystanie po upływie okresu aktualizacji nie wymaga dodatkowej opłaty. Strona licencyjna IronOCR publikuje wszystkie ceny bezpośrednio, bez konsultacji sprzedażowej.
Strukturalne dane bez konfiguracji stref. Po migracji, dane na poziomie słów, linii i akapitów z współrzędnymi ramki ograniczającej są dostępne bezpośrednio na OcrResult — nie są wymagane definicje stref. Pięciopoziomowa hierarchia (Pages, Paragraphs, Lines, Words, Characters) ujawnia współrzędne pozycji i wskaźniki zaufania na element. Aplikacje, które wymagały konfiguracji stref LEADTOOLS w celu uzyskania ustrukturyzowanego wyciągu, otrzymują bogatsze dane z prostszego interfejsu API. Strona z wynikami OCR zawiera podsumowanie kompletnego modelu wyjściowego.
Często Zadawane Pytania
Dlaczego warto przejść z LEADTOOLS OCR na IronOCR?
Typowe czynniki motywujące to eliminacja złożoności interoperacyjności COM, zastąpienie zarządzania licencjami opartego na plikach, uniknięcie rozliczeń za stronę, umożliwienie wdrażania w Dockerze/kontenerach oraz przyjęcie natywnego dla NuGet przepływu pracy, który integruje się ze standardowymi narzędziami .NET.
Jakie są główne zmiany w kodzie podczas migracji z LEADTOOLS OCR do IronOCR?
Zastąp sekwencje inicjalizacji LEADTOOLS OCR instancjonowaniem IronTesseract, usuń zarządzanie cyklem życia COM (jawne wzorce Create/Load/Close) i zaktualizuj nazwy właściwości wyników. W rezultacie znacznie zmniejsza się liczba powtarzających się linii kodu.
Jak zainstalować IronOCR, aby rozpocząć migrację?
Uruchom polecenie „Install-Package IronOcr” w konsoli menedżera pakietów lub „dotnet add package IronOcr” w interfejsie CLI. Pakiety językowe są oddzielnymi pakietami: na przykład „dotnet add package IronOcr.Languages.French” dla języka francuskiego.
Czy IronOCR dorównuje dokładnością OCR LEADTOOLS OCR w przypadku standardowych dokumentów biznesowych?
IronOCR zapewnia wysoką dokładność w przypadku standardowych treści biznesowych, w tym faktur, umów, paragonów i formularzy wypełnionych na komputerze. Filtry przetwarzania wstępnego obrazu (prostowanie, usuwanie szumów, wzmacnianie kontrastu) dodatkowo poprawiają rozpoznawanie w przypadku pogorszonej jakości danych wejściowych.
W jaki sposób IronOCR obsługuje dane językowe, które LEADTOOLS OCR instaluje oddzielnie?
Dane językowe w IronOCR są dystrybuowane jako pakiety NuGet. Polecenie „dotnet add package IronOcr.Languages.German” instaluje obsługę języka niemiećkiego. Nie wymaga to ręcznego umieszczania plików ani podawania ścieżek katalogów.
Czy migracja z LEADTOOLS OCR do IronOCR wymaga zmian w infrastrukturze wdrożeniowej?
IronOCR wymaga mniej zmian w infrastrukturze niż LEADTOOLS OCR. Nie ma ścieżek binarnych SDK, lokalizacji plików licencyjnych ani konfiguracji serwerów licencyjnych. Pakiet NuGet zawiera kompletny silnik OCR, a klucz licencyjny jest ciągiem znaków ustawionym w kodzie aplikacji.
Jak skonfigurować licencjonowanie IronOCR po migracji?
W kodzie uruchamiającym aplikację przypisz IronOcr.License.LicenseKey = „YOUR-KEY”. W Dockerze lub Kubernetesie zapisz klucz jako zmienną środowiskową i odczytaj go podczas uruchamiania. Użyj License.IsValidLicense do sprawdzenia ważności przed przyjęciem ruchu.
Czy IronOCR może przetwarzać pliki PDF w taki sam sposób jak LEADTOOLS OCR?
Tak. IronOCR odczytuje zarówno natywne, jak i zeskanowane pliki PDF. Należy utworzyć instancję IronTesseract, wywołać ocr.Read(input), gdzie input jest ścieżką do pliku PDF lub obiektem OcrPdfInput, a następnie iterować strony OcrResult. Nie jest wymagany oddzielny proces renderowania plików PDF.
W jaki sposób IronOCR radzi sobie z wątkami podczas przetwarzania dużych ilości danych?
IronTesseract można bezpiecznie instancjonować dla każdego wątku. Uruchom jedną instancję na wątek w Parallel.ForEach lub puli zadań, uruchom OCR równolegle i usuń każdą instancję po zakończeniu. Nie jest wymagany żaden stan globalny ani blokowanie.
Jakie formaty wyjściowe obsługuje IronOCR po wyodrębnieniu tekstu?
IronOCR zwraca ustrukturyzowane wyniki, w tym tekst, współrzędne słów, wyniki pewności i strukturę strony. Opcje eksportu obejmują zwykły tekst, PDF z możliwością wyszukiwania oraz obiekty wyników ustrukturyzowanych do dalszego przetwarzania.
Czy ceny IronOCR są bardziej przewidywalne niż ceny LEADTOOLS OCR w przypadku skalowania obciążeń?
IronOCR stosuje licencję wieczystą z opłatą ryczałtową, bez opłat za stronę lub wolumen. Niezależnie od tego, czy przetwarzasz 10 000, czy 10 milionów stron, koszt licencji pozostaje stały. Opcje licencji wolumenowych i zespołowych znajdują się na stronie z cennikiem IronOCR.
Co stanie się z moimi istniejącymi testami po migracji z LEADTOOLS OCR do IronOCR?
Testy sprawdzające wyodrębnioną treść tekstową powinny nadal przechodzić pomyślnie po migracji. Testy weryfikujące wzorce wywołań API lub cykl życia obiektów COM będą wymagały aktualizacji, aby odzwierciedlały prostszy model inicjalizacji i wyników IronOCR.

