Migracja z AWS Textract do IronOCR
Ten przewodnik przedstawia kompletną migrację zAWS Textractdo IronOCR dla .NET dla programistów .NET, którzy potrzebują lokalnego przetwarzania dokumentów bez zależności od chmury. Obejmuje to usuwanie danych uwierzytelniających, usuwanie potoku asynchronicznego, eliminację S3 oraz konkretne zamiany kodu wymagane do przeniesienia każdej operacji Textract do jej odpowiednika w IronOCR.
Dlaczego warto przejść z AWS Textract
AWS Textract to wydajna usługa zarządzana, ale jej architektura wiąże się z kosztami — finansowymi i operacyjnymi — które rosną wraz ze wzrostem obciążenia dokumentami. Zespoły tworzące zaawansowane procesy przetwarzania dokumentów w końcu napotykają wszystkie te przeszkody.
Infrastruktura poświadczeń AWS komplikuje każde wdrożenie. Każde środowisko, w którym działa kod Textract, wymaga poświadczeń AWS: użytkownika lub roli IAM, klucza dostępu i sekretu, konfiguracji regionu, a w przypadku przetwarzania plików PDF dodatkowo uprawnień do zasobnika S3. Oznacza to pięć odrębnych kroków konfiguracyjnych przed przetworzeniem pierwszej strony. Produkcja wdrożeń wymaga zasad rotacji poświadczeń, bezpiecznego wstrzykiwania do kontenerów Docker lub podów Kubernetes oraz monitorowania dla AccessDeniedException, gdy IAM policy drift powoduje ciche awarie.IronOCR wymaga jednego ciągu znaków: klucza licencyjnego, ustawianego jednorazowo podczas uruchamiania.
Opłata za stronę nie ma ograniczeń. Podstawowa stawka $0,0015 za stronę dla DetectDocumentText to dolna granica, a nie koszt. Każdy dokument z tabelami uruchamia AnalyzeDocument za $0,015 za stronę — dziesięć razy więcej niż stawka podstawowa. Formularze zwiększają cenę o kolejne 0,05 USD za stronę. Mieszany przepływ dokumentów wynoszący 100 000 stron miesięcznie z ekstrakcją tabel kosztuje 18 000 USD rocznie, a kwota ta jest resetowana każdego stycznia. Licencja IronOCR Professional kosztuje jednorazowo 2999 USD. Następnie koszt za stronę wynosi zero, niezależnie od objętości.
Asynchroniczny potok PDF jest obciążeniem utrzymaniowym. Przetwarzanie dowolnego dokumentu PDF wielostronicowego przez Textract wymaga pięciu odrębnych faz: przesłania do S3, StartDocumentTextDetection, pętli ankietowej, pobierania wyników ze stronicowaniem oraz czyszczenia S3. Każda faza to niezależny tryb awarii wymagający własnej obsługi błędów, strategii ponownych prób i zarządzania czasem oczekiwania. Zespoły, które wdrażają ten proces do produkcji, konsekwentnie zgłaszają, że poświęcają więcej czasu na jego utrzymanie niż na jego stworzenie.IronOCR odczytuje plik PDF za pomocą dwóch wierszy kodu.
Brak dostępu do Internetu oznacza brak Textract. Kontenery Docker w izolowanych segmentach sieci, serwery lokalne, środowiska odizolowane oraz systemy przemysłowe z ograniczeniami ruchu wychodzącego mają jedną wspólną cechę: Textract jest niedostępny.IronOCR instaluje się jako pakiet NuGet i działa bez żadnych połączeń sieciowych po początkowym pobraniu pakietu.
Dane opuszczają Twoją infrastrukturę przy każdym wywołaniu. Każda operacja OCR w Textract przesyła zawartość dokumentu na serwery Amazon. Dla organizacji opieki zdrowotnej przetwarzających dane medyczne (PHI), wykonawców z sektóra obronnego zajmujących się informacjami o znaczeniu krytycznym (CUI), zespołów prawnych przetwarzających poufną korespondencję oraz instytucji finansowych przetwarzających obrazy kart płatniczych nie jest to opcja konfiguracyjna — jest to ograniczenie architektoniczne, które całkowicie uniemożliwia stosowanie Textract w środowiskach podlegających regulacjom.IronOCR działa na Twoim sprzęcie. Nie ma trybu chmury, który można by wyłączyć; Jedynym trybem jest przetwarzanie lokalne.
Limity stawki ograniczają przepustowość zgrupowań. Domyślny limit StartDocumentTextDetection TPS wynosi 5 żądań na sekundę. Prace wsadowe przetwarzające setki dokumentów wymagają SemaphoreSlim throttlingu, wykładniczego cofania się na ProvisionedThroughputExceededException oraz liczników odzyskiwania stawki. Wniosek o zwiększenie limitu TPS wymaga złożenia formalnego zgłoszenia do pomocy technicznej AWS, przy czym nie ma gwarancji pozytywnego rozpatrzenia.IronOCR przetwarza tak szybko, jak pozwala lokalna CPU — serwer z 16 rdzeniami przetwarza 16 dokumentów jednocześnie przy użyciu zwykłego Parallel.ForEach, bez konieczności negocjacji poziomów usług.
Podstawowy problem
Każde wdrożenie Textract rozpoczyna się od tej ceremonii — zanim będzie można przetworzyć choćby jedną stronę:
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call
// Environment must have:
// AWS_ACCESS_KEY_ID=AKIA...
// AWS_SECRET_ACCESS_KEY=...
// AWS_DEFAULT_REGION=us-east-1
// IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
// IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
// S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
public class TextractOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client; // required for PDFs
public TextractOcrService()
{
// Fails with AmazonClientException if credentials not found in chain
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
}
}
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call
// Environment must have:
// AWS_ACCESS_KEY_ID=AKIA...
// AWS_SECRET_ACCESS_KEY=...
// AWS_DEFAULT_REGION=us-east-1
// IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
// IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
// S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
public class TextractOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client; // required for PDFs
public TextractOcrService()
{
// Fails with AmazonClientException if credentials not found in chain
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
}
}
Imports Amazon
Imports Amazon.Textract
Imports Amazon.S3
' Textract: five environment variables, an IAM role, and an S3 bucket
' — all required before the first OCR call
' Environment must have:
' AWS_ACCESS_KEY_ID=AKIA...
' AWS_SECRET_ACCESS_KEY=...
' AWS_DEFAULT_REGION=us-east-1
' IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
' IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
' S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
Public Class TextractOcrService
Private ReadOnly _textractClient As AmazonTextractClient
Private ReadOnly _s3Client As AmazonS3Client ' required for PDFs
Public Sub New()
' Fails with AmazonClientException if credentials not found in chain
_textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
_s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
End Sub
End Class
IronOCR zastępuje cały łańcuch poświadczeń pojedynczym przypisaniem:
// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
Imports System
' IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
' Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
IronOCR a AWS Textract: porównanie funkcji
Poniższa tabela przedstawia możliwości obu bibliotek w aspektach najbardziej istotnych dla decyzji dotyczących migracji.
| Funkcja | AWS Textract | IronOCR |
|---|---|---|
| Miejsce przetwarzania | Amazon Cloud (obowiązkowe) | Tylko lokalnie / na miejscu |
| Wymagane połączenie z Internetem | Zawsze | Nigdy |
| Konfiguracja poświadczeń | Użytkownik/rola IAM + opcjonalny zasób S3 | Pojedynczy ciąg klucza licencyjnego |
| Wielostronicowy plik PDF | Wymagane jest środowisko testowe S3 + zadanie asynchroniczne | Bezpośredni synchroniczny input.LoadPdf() |
| Plik PDF chroniony hasłem | Nieobsługiwane | input.LoadPdf(path, Password: "x") |
| TIFF wieloklatkowy | Nieobsługiwane | input.LoadImageFrames("multi.tiff") |
| Wymagane asynchroniczne odpytywanie | Tak (dla wszystkich plików PDF) | Nie — domyślnie synchroniczne |
| Limity częstotliwości | 5 TPS domyślnie (StartDocumentTextDetection) | Brak — tylko obciążenie procesora |
| Koszt za stronę | 0,0015–0,065 USD za stronę | 0 USD po zakupie licencji |
| Automatyczne przetwarzanie wstępne | Wewnętrzne, niekonfigurowalne | Wyrównanie, usuwanie szumów, kontrast, binarizacja, wyostrzanie, skalowanie |
| Wyjście w formacie PDF z możliwością wyszukiwania | Niedostępne | result.SaveAsSearchablePdf() |
| Eksport hOCR | Niedostępne | result.SaveAsHocrFile() |
| Odczytywanie BarCode | Niedostępne | ocr.Configuration.ReadBarCodes = true |
| OCR oparte na regionie | Za pomocą AnalyzeDocument + relacje blokowe | CropRectangle(x, y, width, height) |
| Wyniki uporządkowane | Płaski List<Block> filtrowany przez BlockType |
Typowany Pages, Paragraphs, Lines, Words |
| Obsługiwane języki | Dominujący język angielski (wybierz dodatkowy) | Ponad 125 pakietów językowych dostępnych za pośrednictwem NuGet |
| Wielojęzyczne tłumaczenie symultaniczne | Nieobsługiwane | OcrLanguage.French + OcrLanguage.German |
| Wdrożenie w środowisku izolowanym | Niemożliwe | W pełni obsługiwane |
| HIPAA bez BAA | Niemożliwe | Brak zewnętrznego procesora — wyłącznie lokalnie |
| Wdrażanie Docker | Wymagane są wstrzyknięte poświadczenia AWS | Brak poświadczeń — zwykły pakiet NuGet |
| Wielopłatformowe | Zarządzane przez AWS (tylko Linux) | Windows, Linux, macOS, Docker, Azure, AWS EC2 |
| Model licencyjny | Rozliczenie według liczby stron (wydatki bieżące) | Wieczysta jednorazowa ($999 / $1 499 / $2 999) |
Szybki start: Migracja zAWS Textractdo IronOCR
Krok 1: Zastąp pakiet NuGet
Usuń pakiety AWS SDK:
dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
Zainstaluj IronOCR z NuGet:
dotnet add package IronOcr
Krok 2: Aktualizacja przestrzeni nazw
Zastąp wszystkie importy przestrzeni nazw AWS pojedynczą przestrzenią nazw IronOCR:
// Before (AWS Textract)
using Amazon.Textract;
using Amazon.Textract.Model;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;
// After (IronOCR)
using IronOcr;
// Before (AWS Textract)
using Amazon.Textract;
using Amazon.Textract.Model;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;
// After (IronOCR)
using IronOcr;
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Runtime
Imports IronOcr
Krok 3: Inicjalizacja licencji
Dodaj inicjalizację licencji jednorazowo podczas uruchamiania aplikacji. Usuń wszystkie ustawienia zmiennych środowiskowych poświadczeń AWS:
// Remove from startup:
// AWS_ACCESS_KEY_ID environment variable
// AWS_SECRET_ACCESS_KEY environment variable
// AWS_DEFAULT_REGION environment variable
// ~/.aws/credentials file entries
// IAM role bindings on the execution context
// Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Remove from startup:
// AWS_ACCESS_KEY_ID environment variable
// AWS_SECRET_ACCESS_KEY environment variable
// AWS_DEFAULT_REGION environment variable
// ~/.aws/credentials file entries
// IAM role bindings on the execution context
// Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
' Remove from startup:
' AWS_ACCESS_KEY_ID environment variable
' AWS_SECRET_ACCESS_KEY environment variable
' AWS_DEFAULT_REGION environment variable
' ~/.aws/credentials file entries
' IAM role bindings on the execution context
' Add instead:
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
Przykłady migracji kodu
Zastąpienie konstruktóra AmazonTextractClient i konfiguracji IAM
Najbardziej widoczną zmianą związaną z migracją jest inicjalizacja klienta. Textract wymaga klienta z wstrzykiwaniem zależności i podstawową obsługą uwierzytelniania — oznacza to, że zarówno klient, jak i wszystkie jego zależności muszą być wstępnie skonfigurowane w każdym środowisku wdrożeniowym. Każda błędna konfiguracja zawiedzie cicho, dopóki pierwsze wywołanie OCR nie wyrzuci AmazonClientException.
Podejście AWS Textract:
// Requires: NuGet AWSSDK.Textract, AWSSDK.S3
// Requires: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION in environment
// Requires: IAM policy with textract:* and s3:PutObject, s3:DeleteObject
using Amazon.S3;
using Amazon.Textract;
public class DocumentOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client;
private readonly string _stagingBucket;
public DocumentOcrService(IConfiguration config)
{
// Credential chain: environment → ~/.aws/credentials → EC2 instance profile
// Fails if none found — runtime exception, not compile-time
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
_stagingBucket = config["Textract:StagingBucket"]; // bucket must exist and have permissions
}
public async Task<string> ReadImageAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
var response = await _textractClient.DetectDocumentTextAsync(
new DetectDocumentTextRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) }
});
return string.Join("\n", response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
}
}
// Requires: NuGet AWSSDK.Textract, AWSSDK.S3
// Requires: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION in environment
// Requires: IAM policy with textract:* and s3:PutObject, s3:DeleteObject
using Amazon.S3;
using Amazon.Textract;
public class DocumentOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client;
private readonly string _stagingBucket;
public DocumentOcrService(IConfiguration config)
{
// Credential chain: environment → ~/.aws/credentials → EC2 instance profile
// Fails if none found — runtime exception, not compile-time
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
_stagingBucket = config["Textract:StagingBucket"]; // bucket must exist and have permissions
}
public async Task<string> ReadImageAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
var response = await _textractClient.DetectDocumentTextAsync(
new DetectDocumentTextRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) }
});
return string.Join("\n", response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
}
}
Imports Amazon.S3
Imports Amazon.Textract
Imports System.IO
Imports System.Threading.Tasks
Imports Microsoft.Extensions.Configuration
Public Class DocumentOcrService
Private ReadOnly _textractClient As AmazonTextractClient
Private ReadOnly _s3Client As AmazonS3Client
Private ReadOnly _stagingBucket As String
Public Sub New(config As IConfiguration)
' Credential chain: environment → ~/.aws/credentials → EC2 instance profile
' Fails if none found — runtime exception, not compile-time
_textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
_s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
_stagingBucket = config("Textract:StagingBucket") ' bucket must exist and have permissions
End Sub
Public Async Function ReadImageAsync(imagePath As String) As Task(Of String)
Dim bytes = File.ReadAllBytes(imagePath)
Dim response = Await _textractClient.DetectDocumentTextAsync(
New DetectDocumentTextRequest With {
.Document = New Document With {.Bytes = New MemoryStream(bytes)}
})
Return String.Join(vbCrLf, response.Blocks _
.Where(Function(b) b.BlockType = BlockType.LINE) _
.Select(Function(b) b.Text))
End Function
End Class
Podejście IronOCR:
// Requires: NuGet IronOcr
// Requires: one license key string — nothing else
using IronOcr;
public class DocumentOcrService
{
private readonly IronTesseract _ocr;
public DocumentOcrService()
{
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
_ocr = new IronTesseract();
}
public string ReadImage(string imagePath)
{
// No credential chain, no region, no bucket — just read
return _ocr.Read(imagePath).Text;
}
}
// Requires: NuGet IronOcr
// Requires: one license key string — nothing else
using IronOcr;
public class DocumentOcrService
{
private readonly IronTesseract _ocr;
public DocumentOcrService()
{
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
_ocr = new IronTesseract();
}
public string ReadImage(string imagePath)
{
// No credential chain, no region, no bucket — just read
return _ocr.Read(imagePath).Text;
}
}
Imports IronOcr
Public Class DocumentOcrService
Private ReadOnly _ocr As IronTesseract
Public Sub New()
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY" ' or set at Program.vb startup
_ocr = New IronTesseract()
End Sub
Public Function ReadImage(imagePath As String) As String
' No credential chain, no region, no bucket — just read
Return _ocr.Read(imagePath).Text
End Function
End Class
Cała infrastruktura poświadczeń AWS — role IAM, zmienne środowiskowe, pliki ~/.aws/credentials, polityki okresu życia bucketów S3 oraz konfiguracja regionu — jest usuwana. Konstruktor DocumentOcrService przechodzi z 3-miejscowej wstrzykiwanego miejsca zależności z trybami awarii w czasie wykonywania do pojedynczego przypisania licencji. Szczegółowe informacje na temat wzorców wdrażania można znaleźć w przewodniku konfiguracji IronTesseract.
Zastąpienie funkcji StartDocumentTextDetection przetwarzaniem wieloklatkowym TIFF
Asynchroniczne API zadań Textract (StartDocumentTextDetection) jest wymagane dla dowolnego dokumentu wielostronicowego. Typowym schematem w procesach digitalizacji dokumentów jest otrzymywanie zeskanowanych dokumentów w postaci wielo-ramkowych plików TIFF — jedna ramka na każdą zeskanowaną stronę. Textract w ogóle nie akceptuje plików TIFF jako danych wejściowych; Przed rozpoczęciem zadania dokument musi zostać przekonwertowany do formatu PDF i przesłany do S3.IronOCR natywnie odczytuje pliki TIFF zawierające wiele klatek.
Podejście AWS Textract:
// Textract cannot accept TIFF directly — requires PDF conversion + S3 + async job
// This shows the conversion overhead plus the minimum async pipeline
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<string> ProcessScannedTiffAsync(string tiffPath, string s3Bucket)
{
// Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
// Requires a separate PDF conversion library — not shown here
var pdfPath = Path.ChangeExtension(tiffPath, ".pdf");
ConvertTiffToPdf(tiffPath, pdfPath); // external dependency required
// Step 1: Upload converted PDF to S3
var s3Key = $"staging/{Guid.NewGuid()}.pdf";
using (var stream = File.OpenRead(pdfPath))
{
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = s3Bucket,
Key = s3Key,
InputStream = stream
});
}
try
{
// Step 2: Start async detection job
var startResp = await _textractClient.StartDocumentTextDetectionAsync(
new StartDocumentTextDetectionRequest
{
DocumentLocation = new DocumentLocation
{
S3Object = new S3Object { Bucket = s3Bucket, Name = s3Key }
}
});
// Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
GetDocumentTextDetectionResponse pollResp;
do
{
await Task.Delay(5000);
pollResp = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest { JobId = startResp.JobId });
} while (pollResp.JobStatus == JobStatus.IN_PROGRESS);
if (pollResp.JobStatus != JobStatus.SUCCEEDED)
throw new Exception($"Job failed: {pollResp.StatusMessage}");
// Step 4: Collect paginated results
var text = new StringBuilder();
string token = null;
do
{
var page = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest
{
JobId = startResp.JobId,
NextToken = token
});
foreach (var block in page.Blocks.Where(b => b.BlockType == BlockType.LINE))
text.AppendLine(block.Text);
token = page.NextToken;
} while (token != null);
return text.ToString();
}
finally
{
// Step 5: Clean up S3 — orphaned objects accumulate storage costs
await _s3Client.DeleteObjectAsync(s3Bucket, s3Key);
File.Delete(pdfPath); // clean up intermediate PDF
}
}
// Textract cannot accept TIFF directly — requires PDF conversion + S3 + async job
// This shows the conversion overhead plus the minimum async pipeline
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<string> ProcessScannedTiffAsync(string tiffPath, string s3Bucket)
{
// Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
// Requires a separate PDF conversion library — not shown here
var pdfPath = Path.ChangeExtension(tiffPath, ".pdf");
ConvertTiffToPdf(tiffPath, pdfPath); // external dependency required
// Step 1: Upload converted PDF to S3
var s3Key = $"staging/{Guid.NewGuid()}.pdf";
using (var stream = File.OpenRead(pdfPath))
{
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = s3Bucket,
Key = s3Key,
InputStream = stream
});
}
try
{
// Step 2: Start async detection job
var startResp = await _textractClient.StartDocumentTextDetectionAsync(
new StartDocumentTextDetectionRequest
{
DocumentLocation = new DocumentLocation
{
S3Object = new S3Object { Bucket = s3Bucket, Name = s3Key }
}
});
// Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
GetDocumentTextDetectionResponse pollResp;
do
{
await Task.Delay(5000);
pollResp = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest { JobId = startResp.JobId });
} while (pollResp.JobStatus == JobStatus.IN_PROGRESS);
if (pollResp.JobStatus != JobStatus.SUCCEEDED)
throw new Exception($"Job failed: {pollResp.StatusMessage}");
// Step 4: Collect paginated results
var text = new StringBuilder();
string token = null;
do
{
var page = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest
{
JobId = startResp.JobId,
NextToken = token
});
foreach (var block in page.Blocks.Where(b => b.BlockType == BlockType.LINE))
text.AppendLine(block.Text);
token = page.NextToken;
} while (token != null);
return text.ToString();
}
finally
{
// Step 5: Clean up S3 — orphaned objects accumulate storage costs
await _s3Client.DeleteObjectAsync(s3Bucket, s3Key);
File.Delete(pdfPath); // clean up intermediate PDF
}
}
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Text
Imports System.Threading.Tasks
Public Async Function ProcessScannedTiffAsync(tiffPath As String, s3Bucket As String) As Task(Of String)
' Step 0: Convert TIFF to PDF first (Textract does not accept TIFF for async jobs)
' Requires a separate PDF conversion library — not shown here
Dim pdfPath = Path.ChangeExtension(tiffPath, ".pdf")
ConvertTiffToPdf(tiffPath, pdfPath) ' external dependency required
' Step 1: Upload converted PDF to S3
Dim s3Key = $"staging/{Guid.NewGuid()}.pdf"
Using stream = File.OpenRead(pdfPath)
Await _s3Client.PutObjectAsync(New PutObjectRequest With {
.BucketName = s3Bucket,
.Key = s3Key,
.InputStream = stream
})
End Using
Try
' Step 2: Start async detection job
Dim startResp = Await _textractClient.StartDocumentTextDetectionAsync(
New StartDocumentTextDetectionRequest With {
.DocumentLocation = New DocumentLocation With {
.S3Object = New S3Object With {.Bucket = s3Bucket, .Name = s3Key}
}
})
' Step 3: Poll every 5 seconds — can block for 30–120 seconds on large files
Dim pollResp As GetDocumentTextDetectionResponse
Do
Await Task.Delay(5000)
pollResp = Await _textractClient.GetDocumentTextDetectionAsync(
New GetDocumentTextDetectionRequest With {.JobId = startResp.JobId})
Loop While pollResp.JobStatus = JobStatus.IN_PROGRESS
If pollResp.JobStatus <> JobStatus.SUCCEEDED Then
Throw New Exception($"Job failed: {pollResp.StatusMessage}")
End If
' Step 4: Collect paginated results
Dim text = New StringBuilder()
Dim token As String = Nothing
Do
Dim page = Await _textractClient.GetDocumentTextDetectionAsync(
New GetDocumentTextDetectionRequest With {
.JobId = startResp.JobId,
.NextToken = token
})
For Each block In page.Blocks.Where(Function(b) b.BlockType = BlockType.LINE)
text.AppendLine(block.Text)
Next
token = page.NextToken
Loop While token IsNot Nothing
Return text.ToString()
Finally
' Step 5: Clean up S3 — orphaned objects accumulate storage costs
Await _s3Client.DeleteObjectAsync(s3Bucket, s3Key)
File.Delete(pdfPath) ' clean up intermediate PDF
End Try
End Function
Podejście IronOCR:
//IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling
using IronOcr;
public string ProcessScannedTiff(string tiffPath)
{
using var input = new OcrInput();
input.LoadImageFrames(tiffPath); // reads all frames natively
var ocr = new IronTesseract();
var result = ocr.Read(input);
// Access per-page results if needed
foreach (var page in result.Pages)
Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");
return result.Text;
}
//IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling
using IronOcr;
public string ProcessScannedTiff(string tiffPath)
{
using var input = new OcrInput();
input.LoadImageFrames(tiffPath); // reads all frames natively
var ocr = new IronTesseract();
var result = ocr.Read(input);
// Access per-page results if needed
foreach (var page in result.Pages)
Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");
return result.Text;
}
Imports IronOcr
Public Function ProcessScannedTiff(tiffPath As String) As String
Using input As New OcrInput()
input.LoadImageFrames(tiffPath) ' reads all frames natively
Dim ocr As New IronTesseract()
Dim result = ocr.Read(input)
' Access per-page results if needed
For Each page In result.Pages
Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected")
Next
Return result.Text
End Using
End Function
Eliminuje się etap konwersji plików TIFF do formatu PDF, przesyłanie do S3, asynchroniczną pętlę odpytywania, gromadzenie wyników w postaci stron oraz czyszczenie S3. Implementacja IronOCR odczytuje ramki bezpośrednio i zapewnia dostęp do każdej strony przez result.Pages bez żadnej pośredniej konwersji formatu. Przewodnik dotyczący plików wejściowych TIFF i GIF obejmuje wybór klatek w przypadkach, gdy potrzebny jest tylko podzbiór klatek.
Zastąpienie S3 Staging przez pliki PDF z funkcją wyszukiwania
Typowym zastosowaniem Textract jest konwersja zeskanowanych archiwów PDF do formatu umożliwiającego wyszukiwanie. Podejście Textract wymaga przesłania każdego pliku PDF do S3, uruchomienia zadania asynchronicznego, zebrania wyodrębnionego tekstu, a następnie użycia oddzielnej biblioteki PDF do osadzenia tego tekstu jako warstwy z możliwością wyszukiwania.IronOCR skanuje i tworzy plik PDF z możliwością wyszukiwania w ramach jednego wywołania.
Podejście AWS Textract:
// Textract extracts text but cannot produce searchable PDFs
// Requires: S3 upload + async job + separate PDF library to embed the text layer
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<string> ExtractTextForSearchableLayerAsync(
string scannedPdfPath,
string s3Bucket)
{
// Must upload to S3 — cannot pass PDF bytes directly for async jobs
var key = $"ocr-input/{Guid.NewGuid()}.pdf";
await using (var fs = File.OpenRead(scannedPdfPath))
{
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = s3Bucket,
Key = key,
InputStream = fs,
ContentType = "application/pdf"
});
}
string jobId;
try
{
var startResp = await _textractClient.StartDocumentTextDetectionAsync(
new StartDocumentTextDetectionRequest
{
DocumentLocation = new DocumentLocation
{
S3Object = new S3Object { Bucket = s3Bucket, Name = key }
}
});
jobId = startResp.JobId;
}
catch
{
await _s3Client.DeleteObjectAsync(s3Bucket, key);
throw;
}
// Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
GetDocumentTextDetectionResponse pollResp;
int pollAttempts = 0;
const int maxAttempts = 120; // 10 minute timeout
do
{
await Task.Delay(5000);
pollResp = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest { JobId = jobId });
if (++pollAttempts >= maxAttempts)
throw new TimeoutException("Textract job timed out after 10 minutes");
} while (pollResp.JobStatus == JobStatus.IN_PROGRESS);
await _s3Client.DeleteObjectAsync(s3Bucket, key); // cleanup regardless of outcome
if (pollResp.JobStatus != JobStatus.SUCCEEDED)
throw new Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}");
// Return extracted text — caller must use a separate library to produce searchable PDF
return string.Join("\n", pollResp.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
// Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
}
// Textract extracts text but cannot produce searchable PDFs
// Requires: S3 upload + async job + separate PDF library to embed the text layer
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<string> ExtractTextForSearchableLayerAsync(
string scannedPdfPath,
string s3Bucket)
{
// Must upload to S3 — cannot pass PDF bytes directly for async jobs
var key = $"ocr-input/{Guid.NewGuid()}.pdf";
await using (var fs = File.OpenRead(scannedPdfPath))
{
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = s3Bucket,
Key = key,
InputStream = fs,
ContentType = "application/pdf"
});
}
string jobId;
try
{
var startResp = await _textractClient.StartDocumentTextDetectionAsync(
new StartDocumentTextDetectionRequest
{
DocumentLocation = new DocumentLocation
{
S3Object = new S3Object { Bucket = s3Bucket, Name = key }
}
});
jobId = startResp.JobId;
}
catch
{
await _s3Client.DeleteObjectAsync(s3Bucket, key);
throw;
}
// Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
GetDocumentTextDetectionResponse pollResp;
int pollAttempts = 0;
const int maxAttempts = 120; // 10 minute timeout
do
{
await Task.Delay(5000);
pollResp = await _textractClient.GetDocumentTextDetectionAsync(
new GetDocumentTextDetectionRequest { JobId = jobId });
if (++pollAttempts >= maxAttempts)
throw new TimeoutException("Textract job timed out after 10 minutes");
} while (pollResp.JobStatus == JobStatus.IN_PROGRESS);
await _s3Client.DeleteObjectAsync(s3Bucket, key); // cleanup regardless of outcome
if (pollResp.JobStatus != JobStatus.SUCCEEDED)
throw new Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}");
// Return extracted text — caller must use a separate library to produce searchable PDF
return string.Join("\n", pollResp.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
// Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
}
Imports Amazon.S3
Imports Amazon.S3.Model
Imports Amazon.Textract
Imports Amazon.Textract.Model
Public Async Function ExtractTextForSearchableLayerAsync(
scannedPdfPath As String,
s3Bucket As String) As Task(Of String)
' Must upload to S3 — cannot pass PDF bytes directly for async jobs
Dim key = $"ocr-input/{Guid.NewGuid()}.pdf"
Await Using fs = File.OpenRead(scannedPdfPath)
Await _s3Client.PutObjectAsync(New PutObjectRequest With {
.BucketName = s3Bucket,
.Key = key,
.InputStream = fs,
.ContentType = "application/pdf"
})
End Using
Dim jobId As String
Try
Dim startResp = Await _textractClient.StartDocumentTextDetectionAsync(
New StartDocumentTextDetectionRequest With {
.DocumentLocation = New DocumentLocation With {
.S3Object = New S3Object With {.Bucket = s3Bucket, .Name = key}
}
})
jobId = startResp.JobId
Catch
Await _s3Client.DeleteObjectAsync(s3Bucket, key)
Throw
End Try
' Poll — minimum 5s wait; typical 15–60s for a 10-page PDF
Dim pollResp As GetDocumentTextDetectionResponse
Dim pollAttempts As Integer = 0
Const maxAttempts As Integer = 120 ' 10 minute timeout
Do
Await Task.Delay(5000)
pollResp = Await _textractClient.GetDocumentTextDetectionAsync(
New GetDocumentTextDetectionRequest With {.JobId = jobId})
If System.Threading.Interlocked.Increment(pollAttempts) >= maxAttempts Then
Throw New TimeoutException("Textract job timed out after 10 minutes")
End If
Loop While pollResp.JobStatus = JobStatus.IN_PROGRESS
Await _s3Client.DeleteObjectAsync(s3Bucket, key) ' cleanup regardless of outcome
If pollResp.JobStatus <> JobStatus.SUCCEEDED Then
Throw New Exception($"Textract job status: {pollResp.JobStatus} — {pollResp.StatusMessage}")
End If
' Return extracted text — caller must use a separate library to produce searchable PDF
Return String.Join(vbLf, pollResp.Blocks _
.Where(Function(b) b.BlockType = BlockType.LINE) _
.Select(Function(b) b.Text))
' Caller still needs: iTextSharp, PdfSharp, or similar to embed text layer
End Function
Podejście IronOCR:
//IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed
using IronOcr;
public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadPdf(scannedPdfPath);
var result = ocr.Read(input);
// Embed extracted text as searchable layer in one call
result.SaveAsSearchablePdf(outputPath);
Console.WriteLine($"Pages processed: {result.Pages.Length}");
Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
//IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed
using IronOcr;
public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadPdf(scannedPdfPath);
var result = ocr.Read(input);
// Embed extracted text as searchable layer in one call
result.SaveAsSearchablePdf(outputPath);
Console.WriteLine($"Pages processed: {result.Pages.Length}");
Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
Imports IronOcr
Public Sub ConvertToSearchablePdf(scannedPdfPath As String, outputPath As String)
Dim ocr As New IronTesseract()
Using input As New OcrInput()
input.LoadPdf(scannedPdfPath)
Dim result = ocr.Read(input)
' Embed extracted text as searchable layer in one call
result.SaveAsSearchablePdf(outputPath)
Console.WriteLine($"Pages processed: {result.Pages.Length}")
Console.WriteLine($"Overall confidence: {result.Confidence:F1}%")
End Using
End Sub
Logika limitu czasu odpytywania, wzorzec przesyłania i czyszczenia S3 oraz zależność od zewnętrznej biblioteki PDF zostały usunięte. SaveAsSearchablePdf bezpośrednio osadza rozpoznany tekst w pliku wyjściowym, dzięki czemu każda zeskanowana strona jest pełnotekstowo przeszukiwana bez potrzeby drugiej biblioteki. Przewodnik w formacie PDF z funkcją wyszukiwania obejmuje wybór stron, opcje kompresji oraz osadzanie metadanych. Szerszy kontekst procesów OCR plików PDF można znaleźć w przewodniku dotyczącym plików PDF.
Zastąpienie analizy dokumentu poprzez przeglądanie grafu bloków wynikami stron ustrukturyzowanych
Textract's AnalyzeDocument z TABLES i FORMS zwraca płaski List<Block>, gdzie każdy element — linie, słowa, komórki, nagłówki tabel, klucze formularzy, wartości formularzy — jest tego samego Block typu rozróżnianego tylko przez BlockType i połączonego przez tablice ID RelationshipType.CHILD. Odtworzenie struktury logicznej dokumentu wymaga przejścia przez ten graf relacji.IronOCR zwraca hierarchię tekstową: strony, akapity, wiersze, słowa, z których każdy ma dostęp do współrzędnych.
Podejście AWS Textract:
// AnalyzeDocument with TABLES returns blocks that must be graph-traversed
// to determine which words belong to which paragraphs
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<List<DocumentSection>> ExtractDocumentStructureAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
var response = await _textractClient.AnalyzeDocumentAsync(
new AnalyzeDocumentRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) },
FeatureTypes = new List<string> { "TABLES", "FORMS" }
// Note: AnalyzeDocument (TABLES) costs $0.015/page — 10x DetectDocumentText
});
var sections = new List<DocumentSection>();
// Filter LINE blocks for paragraph reconstruction
// LINE blocks are not grouped into paragraphs — must infer from Y proximity
var lineBlocks = response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.OrderBy(b => b.Geometry?.BoundingBox?.Top ?? 0)
.ToList();
// Group lines into pseudo-paragraphs by vertical gap heuristic
var currentParagraph = new List<Block>();
float? lastBottom = null;
const float paragraphGapThreshold = 0.02f; // 2% of page height
foreach (var line in lineBlocks)
{
var top = line.Geometry?.BoundingBox?.Top ?? 0;
var height = line.Geometry?.BoundingBox?.Height ?? 0;
if (lastBottom.HasValue && (top - lastBottom.Value) > paragraphGapThreshold)
{
if (currentParagraph.Any())
{
sections.Add(new DocumentSection
{
Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
LineCount = currentParagraph.Count,
// Confidence: average of all LINE block confidence values
Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
});
currentParagraph.Clear();
}
}
currentParagraph.Add(line);
lastBottom = top + height;
}
if (currentParagraph.Any())
sections.Add(new DocumentSection
{
Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
LineCount = currentParagraph.Count,
Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
});
return sections;
}
public class DocumentSection
{
public string Text { get; set; }
public int LineCount { get; set; }
public float Confidence { get; set; }
}
// AnalyzeDocument with TABLES returns blocks that must be graph-traversed
// to determine which words belong to which paragraphs
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<List<DocumentSection>> ExtractDocumentStructureAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
var response = await _textractClient.AnalyzeDocumentAsync(
new AnalyzeDocumentRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) },
FeatureTypes = new List<string> { "TABLES", "FORMS" }
// Note: AnalyzeDocument (TABLES) costs $0.015/page — 10x DetectDocumentText
});
var sections = new List<DocumentSection>();
// Filter LINE blocks for paragraph reconstruction
// LINE blocks are not grouped into paragraphs — must infer from Y proximity
var lineBlocks = response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.OrderBy(b => b.Geometry?.BoundingBox?.Top ?? 0)
.ToList();
// Group lines into pseudo-paragraphs by vertical gap heuristic
var currentParagraph = new List<Block>();
float? lastBottom = null;
const float paragraphGapThreshold = 0.02f; // 2% of page height
foreach (var line in lineBlocks)
{
var top = line.Geometry?.BoundingBox?.Top ?? 0;
var height = line.Geometry?.BoundingBox?.Height ?? 0;
if (lastBottom.HasValue && (top - lastBottom.Value) > paragraphGapThreshold)
{
if (currentParagraph.Any())
{
sections.Add(new DocumentSection
{
Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
LineCount = currentParagraph.Count,
// Confidence: average of all LINE block confidence values
Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
});
currentParagraph.Clear();
}
}
currentParagraph.Add(line);
lastBottom = top + height;
}
if (currentParagraph.Any())
sections.Add(new DocumentSection
{
Text = string.Join(" ", currentParagraph.Select(b => b.Text)),
LineCount = currentParagraph.Count,
Confidence = (float)currentParagraph.Average(b => b.Confidence ?? 0)
});
return sections;
}
public class DocumentSection
{
public string Text { get; set; }
public int LineCount { get; set; }
public float Confidence { get; set; }
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Linq
Imports System.Threading.Tasks
Public Class DocumentProcessor
Private _textractClient As IAmazonTextract
Public Sub New(textractClient As IAmazonTextract)
_textractClient = textractClient
End Sub
Public Async Function ExtractDocumentStructureAsync(imagePath As String) As Task(Of List(Of DocumentSection))
Dim bytes = File.ReadAllBytes(imagePath)
Dim response = Await _textractClient.AnalyzeDocumentAsync(
New AnalyzeDocumentRequest With {
.Document = New Document With {.Bytes = New MemoryStream(bytes)},
.FeatureTypes = New List(Of String) From {"TABLES", "FORMS"}
})
Dim sections = New List(Of DocumentSection)()
Dim lineBlocks = response.Blocks _
.Where(Function(b) b.BlockType = BlockType.LINE) _
.OrderBy(Function(b) If(b.Geometry?.BoundingBox?.Top, 0)) _
.ToList()
Dim currentParagraph = New List(Of Block)()
Dim lastBottom As Single? = Nothing
Const paragraphGapThreshold As Single = 0.02F
For Each line In lineBlocks
Dim top = If(line.Geometry?.BoundingBox?.Top, 0)
Dim height = If(line.Geometry?.BoundingBox?.Height, 0)
If lastBottom.HasValue AndAlso (top - lastBottom.Value) > paragraphGapThreshold Then
If currentParagraph.Any() Then
sections.Add(New DocumentSection With {
.Text = String.Join(" ", currentParagraph.Select(Function(b) b.Text)),
.LineCount = currentParagraph.Count,
.Confidence = CSng(currentParagraph.Average(Function(b) If(b.Confidence, 0)))
})
currentParagraph.Clear()
End If
End If
currentParagraph.Add(line)
lastBottom = top + height
Next
If currentParagraph.Any() Then
sections.Add(New DocumentSection With {
.Text = String.Join(" ", currentParagraph.Select(Function(b) b.Text)),
.LineCount = currentParagraph.Count,
.Confidence = CSng(currentParagraph.Average(Function(b) If(b.Confidence, 0)))
})
End If
Return sections
End Function
End Class
Public Class DocumentSection
Public Property Text As String
Public Property LineCount As Integer
Public Property Confidence As Single
End Class
Podejście IronOCR:
//IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging
using IronOcr;
public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
var ocr = new IronTesseract();
var result = ocr.Read(imagePath);
var sections = new List<DocumentSection>();
foreach (var page in result.Pages)
{
foreach (var paragraph in page.Paragraphs)
{
sections.Add(new DocumentSection
{
Text = paragraph.Text,
LineCount = paragraph.Lines.Length,
// Coordinate access: exact pixel position on the page
LocationX = paragraph.X,
LocationY = paragraph.Y,
Width = paragraph.Width,
Height = paragraph.Height,
Confidence = paragraph.Confidence
});
}
}
return sections;
}
public class DocumentSection
{
public string Text { get; set; }
public int LineCount { get; set; }
public int LocationX { get; set; }
public int LocationY { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public double Confidence { get; set; }
}
//IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging
using IronOcr;
public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
var ocr = new IronTesseract();
var result = ocr.Read(imagePath);
var sections = new List<DocumentSection>();
foreach (var page in result.Pages)
{
foreach (var paragraph in page.Paragraphs)
{
sections.Add(new DocumentSection
{
Text = paragraph.Text,
LineCount = paragraph.Lines.Length,
// Coordinate access: exact pixel position on the page
LocationX = paragraph.X,
LocationY = paragraph.Y,
Width = paragraph.Width,
Height = paragraph.Height,
Confidence = paragraph.Confidence
});
}
}
return sections;
}
public class DocumentSection
{
public string Text { get; set; }
public int LineCount { get; set; }
public int LocationX { get; set; }
public int LocationY { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public double Confidence { get; set; }
}
Imports IronOcr
Public Function ExtractDocumentStructure(imagePath As String) As List(Of DocumentSection)
Dim ocr As New IronTesseract()
Dim result = ocr.Read(imagePath)
Dim sections As New List(Of DocumentSection)()
For Each page In result.Pages
For Each paragraph In page.Paragraphs
sections.Add(New DocumentSection With {
.Text = paragraph.Text,
.LineCount = paragraph.Lines.Length,
.LocationX = paragraph.X,
.LocationY = paragraph.Y,
.Width = paragraph.Width,
.Height = paragraph.Height,
.Confidence = paragraph.Confidence
})
Next
Next
Return sections
End Function
Public Class DocumentSection
Public Property Text As String
Public Property LineCount As Integer
Public Property LocationX As Integer
Public Property LocationY As Integer
Public Property Width As Integer
Public Property Height As Integer
Public Property Confidence As Double
End Class
Akapity, linie i słowa są bezpośrednio dostępne jako typowane kolekcje z .X, .Y, .Width, .Height, i .Confidence właściwościami. Bez normalizacji BoundingBox, bez heurystyki progu odstępów, bez filtrowania typów bloków. Przewodnik po wynikach odczytu dokumentuje pełną hierarchię OcrResult, w tym dostęp do współrzędnych na poziomie znaku. W przypadku procesów związanych z fakturami i paragonami, które opierają się na tej strukturze, zapoznaj się z samouczkiem dotyczącym OCR faktur.
Zastąpienie przetwarzania wsadowego z ograniczeniami szybkości nieograniczonym wykonywaniem równoległym
Przetwarzanie wsadowe obrazów to miejsce, gdzie limity TPS Textract generują najbardziej widoczne koszty operacyjne. Synchronous API DetectDocumentText jest ograniczone przez stawkę; wysyłanie więcej niż 5 żądań na sekundę generuje ProvisionedThroughputExceededException. Poprawna obsługa wsadów wymaga SemaphoreSlim throttlingu, logiki powtórzeń z cofnięciem oraz starannego księgowania dla będących w toku żądań.IronOCR nie ma ograniczeń szybkości — przepustowość jest ograniczona jedynie dostępnymi rdzeniami procesora.
Podejście AWS Textract:
// Batch processing requires SemaphoreSlim throttle + retry on rate limit exceeded
using Amazon.Textract;
using Amazon.Textract.Model;
using System.Collections.Concurrent;
public async Task<Dictionary<string, string>> ProcessImageBatchAsync(
IReadOnlyList<string> imagePaths,
IProgress<(int Completed, int Total)> progress = null)
{
var results = new ConcurrentDictionary<string, string>();
var semaphore = new SemaphoreSlim(5); // 5 concurrent — Textract default TPS limit
var tasks = new List<Task>();
int completed = 0;
foreach (var path in imagePaths)
{
await semaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
try
{
string text = null;
int retryCount = 0;
while (text == null && retryCount < 3)
{
try
{
var bytes = await File.ReadAllBytesAsync(path);
var response = await _textractClient.DetectDocumentTextAsync(
new DetectDocumentTextRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) }
});
text = string.Join("\n", response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
}
catch (AmazonTextractException ex)
when (ex.ErrorCode == "ProvisionedThroughputExceededException")
{
// Exponential backoff on rate limit — blocks the entire thread
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
retryCount++;
}
}
results[path] = text ?? string.Empty;
var done = Interlocked.Increment(ref completed);
progress?.Report((done, imagePaths.Count));
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
return results.ToDictionary(x => x.Key, x => x.Value);
}
// Batch processing requires SemaphoreSlim throttle + retry on rate limit exceeded
using Amazon.Textract;
using Amazon.Textract.Model;
using System.Collections.Concurrent;
public async Task<Dictionary<string, string>> ProcessImageBatchAsync(
IReadOnlyList<string> imagePaths,
IProgress<(int Completed, int Total)> progress = null)
{
var results = new ConcurrentDictionary<string, string>();
var semaphore = new SemaphoreSlim(5); // 5 concurrent — Textract default TPS limit
var tasks = new List<Task>();
int completed = 0;
foreach (var path in imagePaths)
{
await semaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
try
{
string text = null;
int retryCount = 0;
while (text == null && retryCount < 3)
{
try
{
var bytes = await File.ReadAllBytesAsync(path);
var response = await _textractClient.DetectDocumentTextAsync(
new DetectDocumentTextRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) }
});
text = string.Join("\n", response.Blocks
.Where(b => b.BlockType == BlockType.LINE)
.Select(b => b.Text));
}
catch (AmazonTextractException ex)
when (ex.ErrorCode == "ProvisionedThroughputExceededException")
{
// Exponential backoff on rate limit — blocks the entire thread
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
retryCount++;
}
}
results[path] = text ?? string.Empty;
var done = Interlocked.Increment(ref completed);
progress?.Report((done, imagePaths.Count));
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
return results.ToDictionary(x => x.Key, x => x.Value);
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.Collections.Concurrent
Imports System.IO
Imports System.Threading
Public Async Function ProcessImageBatchAsync(
imagePaths As IReadOnlyList(Of String),
Optional progress As IProgress(Of (Completed As Integer, Total As Integer)) = Nothing) As Task(Of Dictionary(Of String, String))
Dim results As New ConcurrentDictionary(Of String, String)()
Dim semaphore As New SemaphoreSlim(5) ' 5 concurrent — Textract default TPS limit
Dim tasks As New List(Of Task)()
Dim completed As Integer = 0
For Each path In imagePaths
Await semaphore.WaitAsync()
tasks.Add(Task.Run(Async Function()
Try
Dim text As String = Nothing
Dim retryCount As Integer = 0
While text Is Nothing AndAlso retryCount < 3
Try
Dim bytes = Await File.ReadAllBytesAsync(path)
Dim response = Await _textractClient.DetectDocumentTextAsync(
New DetectDocumentTextRequest With {
.Document = New Document With {.Bytes = New MemoryStream(bytes)}
})
text = String.Join(vbLf, response.Blocks _
.Where(Function(b) b.BlockType = BlockType.LINE) _
.Select(Function(b) b.Text))
Catch ex As AmazonTextractException When ex.ErrorCode = "ProvisionedThroughputExceededException"
' Exponential backoff on rate limit — blocks the entire thread
Await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
retryCount += 1
End Try
End While
results(path) = If(text, String.Empty)
Dim done = Interlocked.Increment(completed)
If progress IsNot Nothing Then
progress.Report((done, imagePaths.Count))
End If
Finally
semaphore.Release()
End Try
End Function))
Next
Await Task.WhenAll(tasks)
Return results.ToDictionary(Function(x) x.Key, Function(x) x.Value)
End Function
Podejście IronOCR:
// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores
using IronOcr;
using System.Collections.Concurrent;
public Dictionary<string, string> ProcessImageBatch(
IReadOnlyList<string> imagePaths,
IProgress<(int Completed, int Total)> progress = null)
{
var ocr = new IronTesseract();
var results = new ConcurrentDictionary<string, string>();
int completed = 0;
Parallel.ForEach(imagePaths, path =>
{
var result = ocr.Read(path);
results[path] = result.Text;
var done = Interlocked.Increment(ref completed);
progress?.Report((done, imagePaths.Count));
});
return results.ToDictionary(x => x.Key, x => x.Value);
}
// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores
using IronOcr;
using System.Collections.Concurrent;
public Dictionary<string, string> ProcessImageBatch(
IReadOnlyList<string> imagePaths,
IProgress<(int Completed, int Total)> progress = null)
{
var ocr = new IronTesseract();
var results = new ConcurrentDictionary<string, string>();
int completed = 0;
Parallel.ForEach(imagePaths, path =>
{
var result = ocr.Read(path);
results[path] = result.Text;
var done = Interlocked.Increment(ref completed);
progress?.Report((done, imagePaths.Count));
});
return results.ToDictionary(x => x.Key, x => x.Value);
}
Imports IronOcr
Imports System.Collections.Concurrent
Imports System.Threading
Public Function ProcessImageBatch(
imagePaths As IReadOnlyList(Of String),
Optional progress As IProgress(Of (Completed As Integer, Total As Integer)) = Nothing) As Dictionary(Of String, String)
Dim ocr As New IronTesseract()
Dim results As New ConcurrentDictionary(Of String, String)()
Dim completed As Integer = 0
Parallel.ForEach(imagePaths, Sub(path)
Dim result = ocr.Read(path)
results(path) = result.Text
Dim done = Interlocked.Increment(completed)
If progress IsNot Nothing Then
progress.Report((done, imagePaths.Count))
End If
End Sub)
Return results.ToDictionary(Function(x) x.Key, Function(x) x.Value)
End Function
The SemaphoreSlim, pętla powtórzeń, wykładnicze cofnięcie i bloki przechwytywania ProvisionedThroughputExceededException są usunięte. IronTesseract jest bezpieczny w kontekście wątków, a pojedyncza instancja udostępniona między wątkami obsługuje pełne obciążenie równoległe bez blokad. Na komputerze z 8 rdzeniami przetwarza to 8 dokumentów jednocześnie bez żadnej konfiguracji; na 32 rdzeniach, 32 jednocześnie. Przykład wielowątkowości pokazuje kompletny wzorzec, a przewodnik po optymalizacji szybkości obejmuje dostosowanie konfiguracji silnika dla wdrożeń zorientowanych na przepustowość.
Zastąpienie funkcji AnalyzeDocument Form Extraction funkcją Region-Based CropRectangle OCR
Textract's AnalyzeDocument z FORMS zwraca bloki KEY_VALUE_SET, połączone relacjami RelationshipType.VALUE. Odtwarzanie par klucz-wartość pól formularzy wymaga filtrowania przez EntityTypes.Contains("KEY"), podążania według identyfikatorów relacji VALUE, a następnie pobierania bloków potomnych WORD w celu złożenia tekstu. W przypadku formularzy o stałym formacie, gdzie pozycje pól są znane, CropRectangleIronOCR wyodrębnia każdy element bezpośrednio, bez konieczności przeglądania grafów relacji — i kosztuje zero za stronę.
Podejście AWS Textract:
// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
// $0.05/page for FORMS analysis
var response = await _textractClient.AnalyzeDocumentAsync(
new AnalyzeDocumentRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) },
FeatureTypes = new List<string> { "FORMS" }
});
var allBlocks = response.Blocks;
var fields = new Dictionary<string, string>();
// Find KEY blocks only
var keyBlocks = allBlocks.Where(b =>
b.BlockType == BlockType.KEY_VALUE_SET &&
b.EntityTypes?.Contains("KEY") == true);
foreach (var keyBlock in keyBlocks)
{
// Assemble key text from CHILD WORD blocks
var keyText = GetTextFromBlock(allBlocks, keyBlock);
// Find associated VALUE block via VALUE relationship
var valueBlockId = keyBlock.Relationships?
.FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
.Ids.FirstOrDefault();
if (valueBlockId == null) continue;
var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
if (valueBlock == null) continue;
var valueText = GetTextFromBlock(allBlocks, valueBlock);
fields[keyText.TrimEnd(':')] = valueText;
}
return fields;
}
private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
if (block.Relationships == null) return string.Empty;
var childWordIds = block.Relationships
.Where(r => r.Type == RelationshipType.CHILD)
.SelectMany(r => r.Ids);
return string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
.Select(b => b.Text));
}
// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs
using Amazon.Textract;
using Amazon.Textract.Model;
public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
// $0.05/page for FORMS analysis
var response = await _textractClient.AnalyzeDocumentAsync(
new AnalyzeDocumentRequest
{
Document = new Document { Bytes = new MemoryStream(bytes) },
FeatureTypes = new List<string> { "FORMS" }
});
var allBlocks = response.Blocks;
var fields = new Dictionary<string, string>();
// Find KEY blocks only
var keyBlocks = allBlocks.Where(b =>
b.BlockType == BlockType.KEY_VALUE_SET &&
b.EntityTypes?.Contains("KEY") == true);
foreach (var keyBlock in keyBlocks)
{
// Assemble key text from CHILD WORD blocks
var keyText = GetTextFromBlock(allBlocks, keyBlock);
// Find associated VALUE block via VALUE relationship
var valueBlockId = keyBlock.Relationships?
.FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
.Ids.FirstOrDefault();
if (valueBlockId == null) continue;
var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
if (valueBlock == null) continue;
var valueText = GetTextFromBlock(allBlocks, valueBlock);
fields[keyText.TrimEnd(':')] = valueText;
}
return fields;
}
private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
if (block.Relationships == null) return string.Empty;
var childWordIds = block.Relationships
.Where(r => r.Type == RelationshipType.CHILD)
.SelectMany(r => r.Ids);
return string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
.Select(b => b.Text));
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Threading.Tasks
Public Class InvoiceFieldExtractor
Private _textractClient As AmazonTextractClient
Public Async Function ExtractInvoiceFieldsAsync(imagePath As String) As Task(Of Dictionary(Of String, String))
Dim bytes = File.ReadAllBytes(imagePath)
' $0.05/page for FORMS analysis
Dim response = Await _textractClient.AnalyzeDocumentAsync(
New AnalyzeDocumentRequest With {
.Document = New Document With {.Bytes = New MemoryStream(bytes)},
.FeatureTypes = New List(Of String) From {"FORMS"}
})
Dim allBlocks = response.Blocks
Dim fields As New Dictionary(Of String, String)()
' Find KEY blocks only
Dim keyBlocks = allBlocks.Where(Function(b)
Return b.BlockType = BlockType.KEY_VALUE_SET AndAlso
b.EntityTypes?.Contains("KEY") = True
End Function)
For Each keyBlock In keyBlocks
' Assemble key text from CHILD WORD blocks
Dim keyText = GetTextFromBlock(allBlocks, keyBlock)
' Find associated VALUE block via VALUE relationship
Dim valueBlockId = keyBlock.Relationships?.
FirstOrDefault(Function(r) r.Type = RelationshipType.VALUE)?.
Ids.FirstOrDefault()
If valueBlockId Is Nothing Then Continue For
Dim valueBlock = allBlocks.FirstOrDefault(Function(b) b.Id = valueBlockId)
If valueBlock Is Nothing Then Continue For
Dim valueText = GetTextFromBlock(allBlocks, valueBlock)
fields(keyText.TrimEnd(":"c)) = valueText
Next
Return fields
End Function
Private Function GetTextFromBlock(allBlocks As IList(Of Block), block As Block) As String
If block.Relationships Is Nothing Then Return String.Empty
Dim childWordIds = block.Relationships.
Where(Function(r) r.Type = RelationshipType.CHILD).
SelectMany(Function(r) r.Ids)
Return String.Join(" ", allBlocks.
Where(Function(b) b.BlockType = BlockType.WORD AndAlso childWordIds.Contains(b.Id)).
Select(Function(b) b.Text))
End Function
End Class
Podejście IronOCR:
// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known
using IronOcr;
// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
new Dictionary<string, CropRectangle>
{
{ "InvoiceNumber", new CropRectangle(450, 80, 300, 35) },
{ "InvoiceDate", new CropRectangle(450, 120, 300, 35) },
{ "VendorName", new CropRectangle(50, 160, 400, 35) },
{ "TotalAmount", new CropRectangle(450, 680, 200, 35) },
{ "DueDate", new CropRectangle(450, 720, 200, 35) }
};
public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
var ocr = new IronTesseract();
var fields = new Dictionary<string, string>();
foreach (var zone in InvoiceFieldZones)
{
using var input = new OcrInput();
input.LoadImage(imagePath, zone.Value); // load only the defined region
var result = ocr.Read(input);
fields[zone.Key] = result.Text.Trim();
}
return fields;
}
// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known
using IronOcr;
// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
new Dictionary<string, CropRectangle>
{
{ "InvoiceNumber", new CropRectangle(450, 80, 300, 35) },
{ "InvoiceDate", new CropRectangle(450, 120, 300, 35) },
{ "VendorName", new CropRectangle(50, 160, 400, 35) },
{ "TotalAmount", new CropRectangle(450, 680, 200, 35) },
{ "DueDate", new CropRectangle(450, 720, 200, 35) }
};
public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
var ocr = new IronTesseract();
var fields = new Dictionary<string, string>();
foreach (var zone in InvoiceFieldZones)
{
using var input = new OcrInput();
input.LoadImage(imagePath, zone.Value); // load only the defined region
var result = ocr.Read(input);
fields[zone.Key] = result.Text.Trim();
}
return fields;
}
Imports IronOcr
' CropRectangle extracts each field zone directly — no relationship graph
' Works for any fixed-format form where field positions are known
' Define the field zones for your form template once
Private Shared ReadOnly InvoiceFieldZones As New Dictionary(Of String, CropRectangle) From {
{"InvoiceNumber", New CropRectangle(450, 80, 300, 35)},
{"InvoiceDate", New CropRectangle(450, 120, 300, 35)},
{"VendorName", New CropRectangle(50, 160, 400, 35)},
{"TotalAmount", New CropRectangle(450, 680, 200, 35)},
{"DueDate", New CropRectangle(450, 720, 200, 35)}
}
Public Function ExtractInvoiceFields(imagePath As String) As Dictionary(Of String, String)
Dim ocr As New IronTesseract()
Dim fields As New Dictionary(Of String, String)()
For Each zone In InvoiceFieldZones
Using input As New OcrInput()
input.LoadImage(imagePath, zone.Value) ' load only the defined region
Dim result = ocr.Read(input)
fields(zone.Key) = result.Text.Trim()
End Using
Next
Return fields
End Function
Filtrowanie bloków KEY_VALUE_SET, przechodzenie z RelationshipType.VALUE oraz GetTextFromBlock pomocnicze są eliminowane. Każde pole odczytuje dokładnie ten obszar pikseli, który je zawiera — ani więcej, ani mniej. Przewodnik po OCR bazującym na regionie obejmuje współrzędne CropRectangle oraz jak definiować strefy z wymiarów szablonów. W przypadku procesów związanych z fakturami, wpis na blogu o OCR faktur oraz samouczek skanowania paragonów pokazują pełne procesy ekstrakcji danych z pól.
Dokumentacja APIAWS Textractdo IronOCR
| AWS Textract | Odpowiednik IronOCR |
|---|---|
AmazonTextractClient |
IronTesseract |
AmazonS3Client |
Nie jest wymagane |
DetectDocumentTextRequest |
OcrInput z input.LoadImage() |
DetectDocumentTextResponse |
OcrResult |
AnalyzeDocumentRequest (TABLES/FORMS) |
OcrInput z opcjonalnym CropRectangle |
StartDocumentTextDetectionRequest |
OcrInput z input.LoadPdf() — synchroniczne, bez uruchamiania zadań |
GetDocumentTextDetectionRequest |
Nie dotyczy — wyniki są natychmiastowe |
Document.Bytes |
input.LoadImage(byteArray) lub input.LoadImage(stream) |
S3Object (staging dokumentów) |
Ścieżka pliku lub strumień — nie jest wymagane przygotowanie |
Block (BlockType.LINE) |
result.Lines (typowana kolekcja) |
Block (BlockType.WORD) |
result.Words (typowana kolekcja) |
Block (BlockType.TABLE) |
result.Words pogrupowane według bliskości współrzędnych |
Block (BlockType.KEY_VALUE_SET) |
CropRectangle wyodrębnianie regionu na pole |
Block.Confidence |
word.Confidence / result.Confidence |
Block.Geometry.BoundingBox |
word.X, word.Y, word.Width, word.Height |
RelationshipType.CHILD przegląd |
Bezpośredni dostęp do właściwości w obiektach wynikowych typu |
JobStatus.IN_PROGRESS |
Nie dotyczy — brak stanu asynchronicznego |
JobStatus.SUCCEEDED |
Nie dotyczy — zwrot synchroniczny |
response.NextToken (stronicowanie) |
Nie dotyczy — wyniki nie są podzielone na strony |
ProvisionedThroughputExceededException |
Nie dotyczy — brak limitów TPS |
AccessDeniedException |
Nie dotyczy — brak łańcucha poświadczeń |
input.LoadImageFrames() |
Bezpośrednia obsługa plików TIFF z wieloma ramkami (bez odpowiednika Textract) |
result.SaveAsSearchablePdf() |
Wynik w formacie PDF z możliwością wyszukiwania (bez odpowiednika Textract) |
Typowe problemy związane z migracją i ich rozwiązania
Problem 1: Nieprawidłowa konfiguracja poświadczeń w nowych środowiskach wdrożeniowych
AWS Textract: Konstruktor AmazonTextractClient rozwiązuje poświadczenia z łańcucha: zmienne środowiskowe, ~/.aws/credentials, profil instancji EC2, rola zadania ECS. W nowym kontenerze Docker lub środowisku CI, jeśli żadne z tych elementów nie są skonfigurowane, klient skonstruuje się bez błędu, ale każde wywołanie API wyrzuci AmazonClientException: No credentials specified. Błąd jest niewidoczny aż do momentu uruchomienia.
Rozwiązanie: Całkowicie usuń łańcuch poświadczeń. Ustaw klucz licencyjny IronOCR w zmiennej środowiskowej, która jest łatwiejsza w konfiguracji i nie wymaga zarządzania uprawnieniami IAM:
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
Imports System
' Single environment variable — no IAM, no rotation, no chain resolution
Dim key As String = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
If String.IsNullOrEmpty(key) Then
Throw New InvalidOperationException("IRONOCR_LICENSE environment variable is not set")
End If
IronOcr.License.LicenseKey = key
Problem 2: Miejsca wywołań asynchronicznych w całym kodzie źródłowym
AWS Textract: Ponieważ całe SDK jest wyłącznie asynchroniczne (DetectDocumentTextAsync, StartDocumentTextDetectionAsync, GetDocumentTextDetectionAsync), wszystkie miejsca wywołania Textract propagują async Task<t> przez stos wywołań. Przejście na synchroniczne API IronOCR wymaga podjęcia decyzji w każdej lokalizacji.
Rozwiązanie: W przypadku usług przetwarzania w tle i akcji kontrolera synchroniczne wywołania IronOCRsą bezpieczne w wątkach działających w tle. Opakuj w Task.Run tylko wtedy, gdy istniejący łańcuch wywołań musi pozostać asynchroniczny:
// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
// Offload synchronous OCR to thread pool — keeps controller non-blocking
var text = await Task.Run(() =>
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage(ms.ToArray());
return ocr.Read(input).Text;
});
return Ok(text);
}
// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
// Offload synchronous OCR to thread pool — keeps controller non-blocking
var text = await Task.Run(() =>
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage(ms.ToArray());
return ocr.Read(input).Text;
});
return Ok(text);
}
Imports System.IO
Imports System.Threading.Tasks
Imports Microsoft.AspNetCore.Mvc
Public Class YourController
Inherits Controller
' If the call site must remain async (e.g., an ASP.NET controller action):
Public Async Function ProcessUpload(file As IFormFile) As Task(Of IActionResult)
Using ms As New MemoryStream()
Await file.CopyToAsync(ms)
' Offload synchronous OCR to thread pool — keeps controller non-blocking
Dim text = Await Task.Run(Function()
Dim ocr = New IronTesseract()
Using input As New OcrInput()
input.LoadImage(ms.ToArray())
Return ocr.Read(input).Text
End Using
End Function)
Return Ok(text)
End Using
End Function
End Class
Dla przetworników wsadowych i usług działających w tle, które już uruchomione są na wątkach nie-UI, bezpośrednio wywołaj ocr.Read() synchronicznie — Task.Run dodaje obciążenie bez korzyści w tych kontekstach. Przewodnik po asynchronicznym OCR zawiera zalecane wzorce.
Problem 3: Logika czyszczenia zasobników S3 rozproszona w kodzie
AWS Textract: Każdy kod przesyłany do S3 w celu przetworzenia przez Textract musi zostać usunięty z S3 po zakończeniu zadania. To czyszczenie często żyje w blokach finally i czasami jest pomijane na ścieżkach błędów, powodując osierocone obiekty, które gromadzą koszty przechowywania. Podczas migracji łatwo przeoczyć kod czyszczący S3, ponieważ nie jest on koncepcyjnie powiązany z OCR.
Rozwiązanie: Cały wzorzec przesyłania i czyszczenia w S3 nie ma odpowiednika w IronOCR. Usuń całkowicie wszystkie bloki PutObjectAsync, DeleteObjectAsync, i S3 związane z blokami try/finally.IronOCR odczytuje dane bezpośrednio z lokalnych ścieżek lub strumieni:
// Before: upload → job → poll → extract → cleanup (50+ lines)
// After: two lines, no cleanup required
using var input = new OcrInput();
input.LoadPdf(localPdfPath);
var text = new IronTesseract().Read(input).Text;
// Before: upload → job → poll → extract → cleanup (50+ lines)
// After: two lines, no cleanup required
using var input = new OcrInput();
input.LoadPdf(localPdfPath);
var text = new IronTesseract().Read(input).Text;
Imports IronOcr
Using input As New OcrInput()
input.LoadPdf(localPdfPath)
Dim text As String = New IronTesseract().Read(input).Text
End Using
Po migracji należy wyłączyć zasób S3 staging bucket. Przewodnik po danych wejściowych w formacie PDF oraz przewodnik po danych wejściowych strumieniowych obejmują wszystkie wzorce danych wejściowych, które zastępują dostęp do dokumentów w fazie S3.
Problem 4: Kod przechodzenia przez wykres blokowy nie ma bezpośredniego odpowiednika
AWS Textract: Kod przechodzący przez Block.Relationships aby odtworzyć komórki tabel, pola formularzy lub grupowania akapitów z tablic ID RelationshipType.CHILD jest najbardziej czasochłonnym kodem do zmigrowania. Nie ma jednoznacznego zamiennika, ponieważIronOCR zwraca kolekcje typów zamiast grafu relacji.
Rozwiązanie: Zastąp każdy wzorzec traversal odpowiednią właściwością typu. Wyszukiwanie identyfikatorów relacji staje się bezpośrednim dostępem do właściwości:
// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD &&
childIds.Contains(b.Id))
.Select(b => b.Text));
// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
foreach (var para in page.Paragraphs)
Console.WriteLine(para.Text); // already assembled
// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD &&
childIds.Contains(b.Id))
.Select(b => b.Text));
// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
foreach (var para in page.Paragraphs)
Console.WriteLine(para.Text); // already assembled
' Before: filter by BlockType + traverse relationship IDs
Dim paragraphText As String = String.Join(" ", allBlocks _
.Where(Function(b) b.BlockType = BlockType.WORD AndAlso _
childIds.Contains(b.Id)) _
.Select(Function(b) b.Text))
' After: direct paragraph text (no filtering, no relationship lookup)
For Each page In result.Pages
For Each para In page.Paragraphs
Console.WriteLine(para.Text) ' already assembled
Next
Next
Do rekonstrukcji tabel, które polegały na BlockType.CELL oraz właściwościach ColumnIndex, należy użyć grupowania według współrzędnych słów na result.Words. Przewodnik po czytaniu tabeli obejmuje podejście oparte na współrzędnych.
Problem 5: Bloki przechwytujące wyjątek ProvisionedThroughputExceededException
AWS Textract: Solidny kod przetwarzania wsadowego przechwytuje AmazonTextractException z ErrorCode == "ProvisionedThroughputExceededException" i implementuje ponowne próby z cofnięciem. Ten kod błędu jest pojęciem specyficznym dla Textract i nie ma jego odpowiednika w IronOCR.
Rozwiązanie: Usuń wszystkie bloki przechwytywania ProvisionedThroughputExceededException.IronOCR nie ma ograniczeń dotyczących TPS. Zastąp cały wzorzec ograniczonej partii z Parallel.ForEach:
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
var result = new IronTesseract().Read(path);
results[path] = result.Text;
});
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
var result = new IronTesseract().Read(path);
results[path] = result.Text;
});
Imports System.Threading.Tasks
Parallel.ForEach(imagePaths, Sub(path)
Dim result = New IronTesseract().Read(path)
results(path) = result.Text
End Sub)
Problem 6: Konfiguracja regionu wbudowana w konstruktory klienta
AWS Textract: new AmazonTextractClient(Amazon.RegionEndpoint.USEast1) hardcodes a region. Wdrożenia wieloregionalne wymagają wielu instancji klienta lub dynamicznego wyboru regionu. Gdy Textract rozszerzy obsługę o nowe regiony, kod musi zostać zaktualizowany.
Rozwiązanie:IronOCR nie ma pojęcia regionu. Usuń wszystkie referencje Amazon.RegionEndpoint. Konstruktor IronTesseract nie przyjmuje żadnej konfiguracji sieciowej — przetwarzanie jest lokalne. W przypadku wdrożeń wieloregionalnych w infrastrukturze AWS, gdzie każdy region obsługuje własne zasoby obliczeniowe, każda instancja IronOCR przetwarza dokumenty lokalnie w obrębie tych zasobów, bez wywołań międzyregionowych.
Lista kontrolna migracji AWS Textract
Przed migracją
Sprawdź wszystkie użycia Textract i S3 SDK w kodzie źródłowym:
grep -rn "Amazon.Textract\|AmazonTextractClient\|DetectDocumentText" --include="*.cs" .
grep -rn "StartDocumentTextDetection\|GetDocumentTextDetection\|JobStatus" --include="*.cs" .
grep -rn "AmazonS3Client\|PutObjectRequest\|DeleteObjectAsync" --include="*.cs" .
grep -rn "BlockType\|RelationshipType\|KEY_VALUE_SET" --include="*.cs" .
grep -rn "ProvisionedThroughputExceededException\|AmazonTextractException" --include="*.cs" .
grep -rn "AWSSDK\|Amazon.RegionEndpoint" --include="*.csproj" .
grep -rn "Amazon.Textract\|AmazonTextractClient\|DetectDocumentText" --include="*.cs" .
grep -rn "StartDocumentTextDetection\|GetDocumentTextDetection\|JobStatus" --include="*.cs" .
grep -rn "AmazonS3Client\|PutObjectRequest\|DeleteObjectAsync" --include="*.cs" .
grep -rn "BlockType\|RelationshipType\|KEY_VALUE_SET" --include="*.cs" .
grep -rn "ProvisionedThroughputExceededException\|AmazonTextractException" --include="*.cs" .
grep -rn "AWSSDK\|Amazon.RegionEndpoint" --include="*.csproj" .
Przed napisaniem jakiegokolwiek kodu zastępczego należy:
- Policz wszystkie pliki zawierające
AmazonTextractClient— każde to cel zastąpienia - Lista wszystkich miejsc wywołań
AnalyzeDocument— zanotuj, czy używane sąTABLES,FORMS, czy oba - Zidentyfikuj wszystkie asynchroniczne pętle ankietowe (
do { await Task.Delay } while JobStatus == IN_PROGRESS) - Znajdź wszystkie bloki czyszczenia S3
finally— te są usuwane hurtowo, a nie zastępowane - Policz wszystkie bloki przechwytywania
ProvisionedThroughputExceededException— usunięte, nie zastępowane - Zanotuj całą logikę przeglądania
BlockType.TABLE,BlockType.CELL,BlockType.KEY_VALUE_SET— każdy potrzebuje strukturalnego zastąpienia danych wyjściowych
Migracja kodu
- Usuń
AWSSDK.Textract,AWSSDK.S3, iAWSSDK.Coreze wszystkich plików.csproj - Uruchom
dotnet add package IronOcrw każdym projekcie, który wykonuje OCR - Dodaj
IronOcr.License.LicenseKey = ...raz przy uruchomieniu aplikacji (Program.cslub równoważny) - Usuń wszystkie instrukcje
using Amazon.Textract;,using Amazon.Textract.Model;,using Amazon.S3;,using Amazon.Runtime; - Dodaj
using IronOcr;do każdego pliku, który wcześniej importował przestrzenie nazw Textract - Zamień wywołania konstruktora
AmazonTextractClientwywołaniaminew IronTesseract() - Zamień instancjonowanie
AmazonS3Clienti wszystkie wywołaniaPutObjectAsync/DeleteObjectAsyncbezpośrednimi odczytami ścieżek do plików lub strumieni - Zamień wszystkie wywołania
DetectDocumentTextAsync:var text = ocr.Read(path).Text - Zamień wszystkie
StartDocumentTextDetectionAsync+ pętle ankietowe nainput.LoadPdf(path)+ocr.Read(input) - Zamień wszystkie złożone potoki TIFF (TIFF → PDF → S3 → async) na
input.LoadImageFrames(tiffPath) - Zamień łańcuchy filtrów
BlockType.LINE/BlockType.WORDnaresult.Lines/result.Words - Zamień przegląd relacji
BlockType.KEY_VALUE_SETna wyodrębnianie strefyCropRectangle - Usuń wszystkie bloki przechwytywania
ProvisionedThroughputExceededExceptioni ograniczeniaSemaphoreSlim - Zamień wzorce wsadowe ograniczeń na
Parallel.ForEachużywając wspólnegoIronTesseractinstancji - Usuń wszystkie konfiguracje zmiennych środowiskowych poświadczeń AWS z manifestów wdrożeniowych i potoków CI
- Wyłącz z użycia tymczasowe zasoby S3 po migracji i weryfikacji całego kodu przetwarzającego
Po migracji
- Zweryfikuj, czy aplikacja uruchamia się poprawnie tylko z
IRONOCR_LICENSEustawionym i wszystkie zmienne środowiskowe AWS są usunięte - Uruchom OCR na tych samych przykładowych obrazach, które zostały wcześniej przetworzone przez Textract, i porównaj dokładność wyodrębnionego tekstu
- Bezpośrednio przetwarzaj wielostronicowe pliki PDF i sprawdzaj, czy wszystkie strony zostały wyodrębnione bez korzystania z S3 lub asynchronicznego odpytywania
- Przetwarzaj plik TIFF zawierający wiele ramek i sprawdź, czy liczba stron zgadza się z wynikami Textract dla tego samego dokumentu
- Przetestuj przetwarzanie wsadowe z zestawem 50+ obrazów i potwierdź, że nie występuje równoważnik
ProvisionedThroughputExceededException - Uruchom aplikację w kontenerze Docker bez dostępu do Internetu i sprawdź, czy OCR zakończyło się pomyślnie
- Sprawdź, czy plik PDF z możliwością wyszukiwania otwiera się poprawnie w programie Adobe Reader i umożliwia wyszukiwanie pełnotekstowe
- Potwierdź, że usunięcie
AWS_ACCESS_KEY_IDiAWS_SECRET_ACCESS_KEYz środowiska wdrażania niczego nie zepsuje - Przetestuj wszystkie procesy wyodrębniania formularzy lub faktur w oparciu o znane wyniki z Textract, aby zweryfikować poprawność pól
- Zmierz opóźnienie przetwarzania dla reprezentatywnego zestawu dokumentów i potwierdź wyeliminowanie minimalnego 5-sekundowego czasu oczekiwania na odpytanie
Kluczowe korzyści z migracji do IronOCR
Konsolidacja infrastruktury w jednym pakiecie NuGet. Trzy pakiety AWS SDK, zasób S3 z zasadami cyklu życia, role IAM we wszystkich środowiskach wdrożeniowych oraz procedury rotacji poświadczeń AWS zostały zastąpione jednym pakietem NuGet. Zmiana .csproj jest dotnet remove package AWSSDK.Textract, a następnie dotnet add package IronOcr. Wraz z tym znikają wszystkie dalsze konsekwencje zarządzania poświadczeniami AWS — audyty bezpieczeństwa, harmonogramy rotacji, higiena zmiennych środowiskowych, konfiguracja sekretów Docker.
Przewidywalny całkowity koszt posiadania. Model rozliczeń za stronę generuje koszty zmienne, które rosną w nieskończoność wraz z objętością dokumentów. Po migracji do IronOCR koszt przetworzenia każdej dodatkowej strony wynosi zero, niezależnie od ilości. Zespół przetwarzający 200 000 stron miesięcznie po stawce Textract AnalyzeDocument dla tabel wydaje $36 000 rocznie tylko na OCR. Licencja IronOCR Enterprise w cenie 2999 USD zwraca ten koszt w mniej niż 31 dni dzięki uniknięciu kosztów. Pełne informacje na temat poziomów licencji oraz warunków redystrybucji SaaS można znaleźć na stronie licencyjnej IronOCR.
Przetwarzanie synchroniczne eliminuje złożoność pięciofazowego przetwarzania asynchronicznego. Przesyłanie do S3, uruchomienie zadania, pętla odpytywania, zbieranie wyników w formacie paginowanym oraz blok końcowy czyszczenia reprezentują pięć niezależnych trybów awarii w potoku przetwarzania plików PDF w Textract. Po migracji kod dotyczący pliku PDF zajmuje zaledwie dwa wiersze. Obsługa błędów sprowadza się do pojedynczego try/catch wokół wywołania ocr.Read(). Logika czasu oczekiwania na pętlę, pętla do/while JobStatus == IN_PROGRESS, akumulator stronicowania nextToken — żadna z tych koncepcji nie istnieje w kodzie bazującym na IronOCR. Obciążenie związane z utrzymaniem kodu zmniejsza się proporcjonalnie do ilości usuniętego kodu.
Pełna elastyczność wdrożenia. Textract wymaga dostępu do Internetu przy każdym wywołaniu funkcji OCR.IronOCR wymaga dostępu do Internetu wyłącznie w celu pobrania pakietu NuGet podczas instalacji. Następnie działa w sieciach typu air-gapped, kontenerach Docker bez reguł wychodzących, serwerach lokalnych oraz instancjach AWS EC2 bez dostępu do Textract. Ten sam plik binarny, który działa w środowisku produkcyjnym na serwerze Windows Server, działa również w kontenerze Linux. Przewodnik wdrażania Docker oraz przewodnik wdrażania Linux obejmują konkretną konfigurację dla środowisk kontenerowych, które wcześniej nie mogły korzystać z Textract.
Suwerenność danych osiągnięta dzięki architekturze, a nie konfiguracji.IronOCR nie posiada trybu transmisji sieciowej, który można wyłączyć — jedynym trybem jest przetwarzanie lokalne. Dokumenty przetwarzane przez IronOCR nigdy nie opuszczają komputera hosta. W przypadku aplikacji medycznych przetwarzających dane PHI, aplikacji obronnych przetwarzających dane CUI oraz aplikacji finansowych przetwarzających obrazy kart płatniczych jest to podejście zgodne z przepisami, które nie wymaga negocjacji umowy BAA, konfiguracji regionalnej lokalizacji danych ani audytu zasad przetwarzania danych przez Amazon. Zakres zgodności to granice Twojej własnej infrastruktury, nad którą już sprawujesz kontrolę. Strona produktu IronOCR zawiera pełne podsumowanie funkcji oraz dokumentację wdrożeniową dla zespołów przeprowadzających przegląd zgodności.
Konfigurowalne przetwarzanie wstępne do kontroli jakości dokumentów. Wewnętrzne przetwarzanie wstępne Textract nie jest dostępne ani konfigurowalne. Gdy zeskanowany dokument daje słabe wyniki, jedynym rozwiązaniem jest zaakceptowanie wyniku lub ponowne skanowanie.IronOCR eksponuje pełny potok przetwarzania wstępnego: Deskew(), DeNoise(), Contrast(), Binarize(), Sharpen(), Scale(), Dilate(), Erode(), i DeepCleanBackgroundNoise(). Zespoły, które przeniosły dokumenty z Textract z powodu wyników o niskim poziomie pewności w przypadku trudnych skanów, mogą bezpośrednio zająć się przyczyną źródłową, stosując filtry dopasowane do konkretnej wady — obrotu, szumu, niskiego kontrastu lub niewystarczającej rozdzielczości. Strona poświęcona funkcjom przetwarzania wstępnego oraz przewodnik po korekcji jakości obrazu zawierają informacje na temat dostępnych filtrów i ich odpowiednich zastosowań.
Często Zadawane Pytania
Dlaczego warto przejść z Amazon Textract 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 Amazon Textract do IronOCR?
Zastąp sekwencje inicjalizacji Amazon Textract 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 Amazon Textract 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 Amazon Textract 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 Amazon Textract do IronOCR wymaga zmian w infrastrukturze wdrożeniowej?
IronOCR wymaga mniej zmian w infrastrukturze niż Amazon Textract. 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 Amazon Textract?
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ż Amazon Textract 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 Amazon Textract 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.

