Migrando do AWS Textract para o IronOCR
Este guia descreve uma migração completa doAWS Textractpara o IronOCR para desenvolvedores .NET que precisam de processamento de documentos local sem dependências da nuvem. Este documento aborda a remoção das credenciais, a eliminação do pipeline assíncrono, a eliminação do S3 e as substituições de código específicas necessárias para migrar cada operação do Textract para seu equivalente no IronOCR.
Por que migrar do AWS Textract?
OAWS Textracté um serviço gerenciado eficiente, mas sua arquitetura impõe custos — financeiros e operacionais — que aumentam conforme o volume de trabalho de documentos cresce. As equipes que desenvolvem fluxos de trabalho robustos para processamento de documentos acabam se deparando com todos esses pontos de atrito.
A infraestrutura de credenciais da AWS complica cada implantação. Todo ambiente que executa código Textract precisa de credenciais da AWS: usuário ou função do IAM, chave de acesso e segredo, configuração de região e — para processamento de PDF — permissões de bucket do S3. Isso representa cinco etapas de configuração distintas antes do processamento da primeira página. Implantações em produção requerem políticas de rotação de credenciais, injeção segura em contêineres Docker ou pods Kubernetes, e monitoramento para AccessDeniedException quando o desvio de política IAM causa falhas silenciosas. O IronOCR requer apenas uma sequência de caracteres: uma chave de licença, definida uma única vez na inicialização.
Preço por página não tem teto. A tarifa base de $0.0015 por página para DetectDocumentText é o piso, não o custo. Qualquer documento com tabelas aciona AnalyzeDocument a $0.015 por página — dez vezes a tarifa base. Os formulários adicionam mais US$ 0,05 por página. Um fluxo de trabalho misto de documentos, com 100.000 páginas por mês e extração de tabelas, custa US$ 18.000 por ano, e esse valor é zerado todo mês de janeiro. A licença Professional do IronOCR custa US$ 2.999,00 em uma única compra. Depois disso, o custo por página é zero, independentemente do volume.
O Pipeline PDF Assíncrono é um Passivo de Manutenção. Processar qualquer PDF de várias páginas através do Textract requer cinco fases distintas: upload S3, StartDocumentTextDetection, loop de polling, recuperação paginada de resultados, e limpeza S3. Cada fase é um modo de falha independente que requer seu próprio tratamento de erro, estratégia de tentativa e gerenciamento de tempo limite. As equipes que implementam esse pipeline em produção relatam consistentemente gastar mais tempo em sua manutenção do que gastaram em sua construção. O IronOCR lê um PDF com duas linhas de código.
Sem acesso à internet, não há Textract. Contêineres Docker em segmentos de rede isolados, servidores locais, ambientes de pesquisa isolados da internet e sistemas industriais com restrições de tráfego de saída compartilham uma característica: o Textract está indisponível. O IronOCR é instalado como um pacote NuGet e funciona sem qualquer chamada de rede após o download inicial do pacote.
Os dados saem da sua infraestrutura a cada chamada. Cada operação de OCR com o Textract transmite o conteúdo do documento para os servidores da Amazon. Para organizações de saúde que processam informações de saúde protegidas (PHI), empresas contratadas pela área de defesa que lidam com informações controladas não classificadas (CUI), equipes jurídicas que processam comunicações privilegiadas e instituições financeiras que processam imagens de cartões de pagamento, isso não é uma opção de configuração — é uma restrição arquitetônica que proíbe completamente o uso do Textract em ambientes regulamentados. O IronOCR é executado no seu hardware. Não existe um modo nuvem para desativar; O processamento local é o único modo.
Limites de Taxa Limitam o Throughput em Lote. O limite de TPS padrão StartDocumentTextDetection é de 5 requisições por segundo. Trabalhos em lote processando centenas de documentos requerem SemaphoreSlim de limitação, retrocesso exponencial em ProvisionedThroughputExceededException, e temporizadores de reabastecimento de taxa. Solicitar um aumento de TPS requer a abertura de um chamado formal de suporte da AWS, sem garantia de resultado.IronOCR processa tão rápido quanto a CPU local permite — um servidor de 16 núcleos processa 16 documentos simultaneamente com um simples Parallel.ForEach, sem necessidade de negociação de camada de serviço.
O problema fundamental
Toda implementação do Textract começa com essa cerimônia — antes mesmo de uma única página ser processada:
// 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
O IronOCR substitui toda a cadeia de credenciais por uma única atribuição:
// 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 vs AWS Textract: Comparação de Recursos
A tabela a seguir mapeia as capacidades de ambas as bibliotecas nas dimensões mais relevantes para as decisões de migração.
| Recurso | AWS Textract | IronOCR |
|---|---|---|
| Local de processamento | Nuvem da Amazon (obrigatório) | Somente local/no local |
| É necessário ter acesso à internet. | Sempre | Nunca |
| Configuração de credenciais | Usuário/função do IAM + bucket S3 opcional | string de chave de licença única |
| PDF com várias páginas | Requer ambiente de preparação S3 + tarefa assíncrona | Execução direta síncrona input.LoadPdf() |
| PDF protegido por senha | Não suportado | input.LoadPdf(path, Password: "x") |
| TIFF de múltiplos quadros | Não suportado | input.LoadImageFrames("multi.tiff") |
| Sondagem assíncrona necessária | Sim (para todos os PDFs) | Não — síncrono por padrão |
| Limites de taxa | 5 TPS padrão (StartDocumentTextDetection) | Nenhum — somente os que dependem da CPU |
| Custo por página | US$ 0,0015 a US$ 0,065 por página | US$ 0 após a compra da licença |
| Pré-processamento automático | Interno, não configurável | Correção de distorção, redução de ruído, contraste, binarização, nitidez, escala |
| Saída em PDF pesquisável | Não disponível | result.SaveAsSearchablePdf() |
| Exportação hOCR | Não disponível | result.SaveAsHocrFile() |
| Leitura de código de barras | Não disponível | ocr.Configuration.ReadBarCodes = true |
| OCR baseado em região | Por meio de AnalyzeDocument + relações de bloco | CropRectangle(x, y, width, height) |
| Resultados Estruturados | Plano List<Block> filtrado por BlockType |
Digitado Pages, Paragraphs, Lines, Words |
| Idiomas suportados | Inglês como língua dominante (selecione outras opções) | Mais de 125 pacotes de idiomas via NuGet |
| Simultâneo em vários idiomas | Não suportado | OcrLanguage.French + OcrLanguage.German |
| Implantação isolada da internet | Não é possível. | Suporte completo |
| HIPAA sem BAA | Não é possível. | Sem processador externo — somente para instalação local |
| Implantação do Docker | Requer credenciais da AWS injetadas. | Sem credenciais — pacote NuGet simples |
| Multiplataforma | Gerenciado pela AWS (somente Linux) | Windows, Linux, macOS, Docker, Azure, AWS EC2 |
| Modelo de licenciamento | Cobrança por página (gasto contínuo) | Perpétuo por única vez ($999 / $1,499 / $2,999) |
Guia rápido: Migração doAWS Textractpara o IronOCR
Passo 1: Substitua o pacote NuGet
Remova os pacotes do SDK da AWS:
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
Instale o IronOCR a partir do NuGet :
dotnet add package IronOcr
Etapa 2: Atualizar Namespaces
Substitua todas as importações de namespace da AWS pelo namespace único do 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
Etapa 3: Inicializar a licença
Adicione a inicialização da licença uma única vez na inicialização do aplicativo. Remova todas as configurações de variáveis de ambiente de credenciais da 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"
Exemplos de migração de código
Substituindo o construtor do AmazonTextractClient e a configuração do IAM
A mudança mais visível durante a migração é a inicialização do cliente. O Textract requer um cliente com injeção de dependência e resolução de credenciais subjacente — o que significa que tanto o cliente quanto todas as suas dependências devem ser pré-configurados em cada ambiente de implantação. Qualquer configuração incorreta falha silenciosamente até a primeira chamada de OCR lançar AmazonClientException.
Abordagem do 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
Abordagem 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
Toda a infraestrutura de credenciais AWS — funções IAM, variáveis de ambiente, arquivos ~/.aws/credentials, políticas de ciclo de vida de bucket S3, e configuração de região — é removida. O construtor DocumentOcrService passa de um local de injeção de 3 dependências com modos de falha em tempo de execução para uma única atribuição de licença. Para obter detalhes sobre os padrões de implantação, consulte o guia de configuração do IronTesseract .
Substituindo a detecção de texto do documento inicial pelo processamento de múltiplos quadros TIFF.
O API de trabalho assíncrono do Textract (StartDocumentTextDetection) é necessário para qualquer documento de múltiplas páginas. Um padrão comum nos fluxos de trabalho de digitalização de documentos é o recebimento de documentos digitalizados como TIFFs multiframe — um frame por página digitalizada. O Textract não aceita entrada TIFF diretamente; O documento deve ser convertido para PDF e carregado no S3 antes que uma tarefa possa ser iniciada. O IronOCR lê TIFFs com múltiplos quadros nativamente.
Abordagem do 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
Abordagem 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
A etapa de conversão de TIFF para PDF, o upload para o S3, o loop de verificação assíncrona, o acúmulo de resultados paginados e a limpeza do S3 são todos eliminados. A implementação do IronOCR lê quadros diretamente e fornece acesso por página através de result.Pages sem qualquer conversão de formato intermediário. O guia de entrada TIFF e GIF aborda a seleção de quadros para casos em que apenas um subconjunto de quadros é necessário.
Substituindo o armazenamento temporário S3 por saída em PDF pesquisável
Um caso de uso comum do Textract é a conversão de arquivos PDF digitalizados em formato pesquisável. A abordagem Textract exige o upload de cada PDF para o S3, a execução da tarefa assíncrona, a coleta do texto extraído e, em seguida, o uso de uma biblioteca PDF separada para incorporar esse texto como uma camada pesquisável. O IronOCR realiza a digitalização e gera um PDF pesquisável em uma única chamada.
Abordagem do 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
Abordagem 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
A lógica de tempo limite de sondagem, o padrão de upload e limpeza do S3 e a dependência da biblioteca PDF externa foram todos removidos. SaveAsSearchablePdf incorpora o texto reconhecido diretamente no arquivo de saída, tornando cada página escaneada totalmente pesquisável em texto sem uma segunda biblioteca. O guia em PDF pesquisável aborda seleção de páginas, opções de compressão e incorporação de metadados. Para um contexto mais amplo sobre fluxos de trabalho de OCR em PDF, consulte o guia de entrada de PDF .
Substituindo a travessia do grafo de blocos do AnalyzeDocument por resultados de página estruturados
O AnalyzeDocument do Textract com TABLES e FORMS retorna um List<Block> plano onde todos os elementos — linhas, palavras, células, cabeçalhos de tabelas, chaves de formulários, valores de formulários — são do mesmo tipo Block distinguidos apenas por BlockType e vinculados por arrays de IDs RelationshipType.CHILD. Reconstruir a estrutura lógica de um documento requer percorrer esse grafo de relacionamentos. O IronOCR retorna uma hierarquia tipada: páginas, parágrafos, linhas, palavras, cada um com acesso por coordenadas.
Abordagem do 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
Abordagem 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
Parágrafos, linhas e palavras são diretamente acessíveis como coleções digitadas com propriedades .X, .Y, .Width, .Height, e .Confidence. Sem normalização de BoundingBox, sem heurísticas de limite de lacuna, sem filtragem de tipo de bloco. O guia de leitura de resultados documenta toda a hierarquia OcrResult incluindo acesso a coordenadas em nível de caractere. Para fluxos de trabalho de faturas e recibos que dependem dessa estrutura, consulte o tutorial de OCR de faturas .
Substituindo o processamento em lote com taxa limitada pela execução paralela irrestrita.
Processamento de imagens em lote é onde os limites de TPS do Textract produzem o custo operacional mais visível. O DetectDocumentText síncrono da API é limitado por taxa; enviar mais de 5 requisições por segundo produz ProvisionedThroughputExceededException. O tratamento correto em lote requer SemaphoreSlim de limitação, lógica de tentativa com retrocesso, e contabilidade cuidadosa para requisições em andamento. O IronOCR não possui limite de taxa — a capacidade de processamento é limitada apenas pelos núcleos de CPU disponíveis.
Abordagem do 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
Abordagem 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
O SemaphoreSlim, o loop de tentativa, o retrocesso exponencial, e os blocos de captura ProvisionedThroughputExceededException são todos removidos. IronTesseract é seguro para threads e uma única instância compartilhada entre threads lida com a carga paralela completa sem bloqueios. Em uma máquina com 8 núcleos, isso processa 8 documentos simultaneamente sem nenhuma configuração; Em 32 núcleos, 32 simultaneamente. O exemplo de multithreading mostra o padrão completo e o guia de otimização de velocidade aborda o ajuste da configuração do mecanismo para implantações focadas em throughput.
Substituindo a extração de formulários do AnalyzeDocument pelo OCR CropRectangle baseado em região.
O AnalyzeDocument do Textract com FORMS retorna blocos KEY_VALUE_SET vinculados por relacionamentos RelationshipType.VALUE. Reconstruir pares chave-valor de campos de formulário requer filtragem por EntityTypes.Contains("KEY"), seguindo IDs de relacionamento VALUE, e então buscando blocos filhos WORD para montar o texto. Para formulários de formato fixo onde as posições dos campos são conhecidas, o CropRectangle do IronOCR extrai cada campo diretamente sem travessia de gráfico de relacionamento — e custa zero por página.
Abordagem do 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
Abordagem 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
A filtragem de bloco KEY_VALUE_SET, a travessia RelationshipType.VALUE, e o assistente GetTextFromBlock são eliminados. Cada campo lê exatamente a região de pixels que o contém — nem mais, nem menos. O guia de OCR baseado em região cobre coordenadas CropRectangle e como definir zonas a partir de medidas de template. Para fluxos de trabalho específicos de faturas, a postagem do blog sobre OCR de faturas e o tutorial de digitalização de recibos demonstram pipelines completos de extração de campos.
Referência de mapeamento da APIAWS Textractpara o IronOCR
| AWS Textract | Equivalente de IronOCR |
|---|---|
AmazonTextractClient |
IronTesseract |
AmazonS3Client |
Não é necessário |
DetectDocumentTextRequest |
OcrInput com input.LoadImage() |
DetectDocumentTextResponse |
OcrResult |
AnalyzeDocumentRequest (TABELAS/FÓRMS) |
OcrInput com CropRectangle opcional |
StartDocumentTextDetectionRequest |
OcrInput com input.LoadPdf() — síncrono, sem início de trabalho |
GetDocumentTextDetectionRequest |
Não aplicável — os resultados são imediatos. |
Document.Bytes |
input.LoadImage(byteArray) ou input.LoadImage(stream) |
S3Object (encenação de documento) |
Caminho do arquivo ou fluxo — sem necessidade de preparação prévia |
Block (BlockType.LINE) |
result.Lines (coleção digitada) |
Block (BlockType.WORD) |
result.Words (coleção digitada) |
Block (BlockType.TABLE) |
result.Words agrupado por proximidade de coordenadas |
Block (BlockType.KEY_VALUE_SET) |
CropRectangle extração por campo de região |
Block.Confidence |
word.Confidence / result.Confidence |
Block.Geometry.BoundingBox |
word.X, word.Y, word.Width, word.Height |
RelationshipType.CHILD de travessia |
Acesso direto a propriedades em objetos de resultado tipados |
JobStatus.IN_PROGRESS |
Não aplicável — nenhum estado assíncrono |
JobStatus.SUCCEEDED |
Não aplicável — retorno síncrono |
response.NextToken (paginaçação) |
Não aplicável — resultados não paginados |
ProvisionedThroughputExceededException |
Não aplicável — sem limites de TPS |
AccessDeniedException |
Não aplicável — sem cadeia de credenciais |
input.LoadImageFrames() |
Suporte direto a TIFF com múltiplos quadros (sem equivalente em Textract) |
result.SaveAsSearchablePdf() |
Saída em PDF pesquisável (sem equivalente em Textract) |
Problemas e soluções comuns em migrações
Problema 1: Credenciais mal configuradas em novos ambientes de implantação
AWS Textract: O construtor AmazonTextractClient resolve credenciais de uma cadeia: variáveis de ambiente, ~/.aws/credentials, perfil de instância EC2, papel de tarefa ECS. Em um novo contêiner Docker ou ambiente CI, se nenhum desses estiver configurado, o cliente é construído sem erro mas cada chamada de API lança AmazonClientException: No credentials specified. A falha é invisível até o momento da execução.
Solução: Remova completamente a cadeia de credenciais. Defina a chave de licença do IronOCR a partir de uma variável de ambiente, que é mais simples de configurar e não requer permissões do IAM para gerenciar:
// 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
Problema 2: Locais de Chamada Assíncrona em Toda a Base de Código
AWS Textract: Porque todo o SDK é apenas assíncrono (DetectDocumentTextAsync, StartDocumentTextDetectionAsync, GetDocumentTextDetectionAsync), todos os locais de chamada do Textract propagam async Task<t> através da cadeia de chamadas. A migração para a API síncrona do IronOCR requer uma decisão em cada local.
Solução: Para serviços de processamento em segundo plano e ações do controlador, as chamadas síncronas do IronOCR são seguras em threads em segundo plano. Envolva em Task.Run apenas quando a cadeia de chamadas existente deve continuar assíncrona:
// 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
Para processadores em lote e serviços de fundo que já estão sendo executados em threads não-UI, chame ocr.Read() de forma síncrona — Task.Run adiciona sobrecarga sem benefício nesses contextos. O guia de OCR assíncrono aborda os padrões recomendados.
Problema 3: Lógica de limpeza do bucket S3 espalhada pelo código
AWS Textract: Qualquer código que seja carregado para o S3 para processamento pelo Textract também deve ser excluído do S3 após a conclusão da tarefa. Esta limpeza frequentemente reside em blocos finally e às vezes é omitida em caminhos de erro, causando objetos órfãos que acumulam custos de armazenamento. Durante a migração, o código de limpeza do S3 pode passar despercebido facilmente, pois não está conceitualmente relacionado ao OCR.
Solução: Todo o padrão de upload e limpeza do S3 não possui equivalente no IronOCR. Exclua todos os blocos PutObjectAsync, DeleteObjectAsync, e relacionados ao S3, try/finally inteiramente. O IronOCR lê diretamente de caminhos ou fluxos locais:
// 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
Desative o bucket de preparação S3 após a migração. O guia de entrada de PDF e o guia de entrada de fluxo abrangem todos os padrões de entrada que substituem o acesso a documentos armazenados em estágios no S3.
Problema 4: O código de percurso em grafo de blocos não possui equivalente direto.
AWS Textract: O código que atravessa Block.Relationships para reconstruir células de tabela, campos de formulário, ou agrupamentos de parágrafos a partir de arrays de IDs RelationshipType.CHILD é o código mais demorado para migrar. Não existe uma substituição direta porque o IronOCR retorna coleções tipadas em vez de um grafo de relacionamento.
Solução: Substitua cada padrão de percurso pela propriedade tipada apropriada. A consulta de IDs de relacionamento passa a permitir o acesso direto à propriedade:
// 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
Para reconstrução de tabelas que dependia de BlockType.CELL e de propriedades ColumnIndex, use agrupamento de coordenadas de palavras em result.Words. O guia de leitura de tabelas aborda a abordagem baseada em coordenadas.
Problema 5: Blocos Catch da exceção ProvisionedThroughputExceededException
AWS Textract: Código robusto de processamento em lote captura AmazonTextractException com ErrorCode == "ProvisionedThroughputExceededException" e implementa tentativas com retrocesso. Este código de erro é um conceito específico do Textract, sem equivalente no IronOCR.
Solução: Excluir todos os blocos de captura ProvisionedThroughputExceededException. O IronOCR não possui limite de TPS. Substitua todo o padrão de lote limitado com 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)
Problema 6: Configuração de região integrada aos construtores do cliente
AWS Textract: new AmazonTextractClient(Amazon.RegionEndpoint.USEast1) define codificadamente uma região. Implantações em várias regiões exigem múltiplas instâncias de cliente ou seleção dinâmica de região. Quando o Textract adiciona suporte a novas regiões, o código precisa ser atualizado.
Solução: O IronOCR não possui o conceito de região. Remova todas as referências Amazon.RegionEndpoint. O construtor IronTesseract não requer configuração de rede — o processamento é local. Para implantações em várias regiões na infraestrutura da AWS, onde cada região executa seu próprio poder computacional, cada instância do IronOCR processa os documentos localmente dentro desse limite computacional, sem chamadas entre regiões.
Lista de verificação de migração do AWS Textract
Pré-migração
Audite todo o uso dos SDKs Textract e S3 na base de código:
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" .
Faça um inventário dos seguintes itens antes de escrever qualquer código de substituição:
- Conte todos os arquivos contendo
AmazonTextractClient— cada um é um alvo de substituição - Liste todos os locais de chamada
AnalyzeDocument— note seTABLES,FORMS, ou ambos são usados - Identifique todos os loops de polling assíncronos (
do { await Task.Delay } while JobStatus == IN_PROGRESS) - Encontre todos os blocos de limpeza S3
finally— eles são excluídos totalmente, não substituídos - Conte todos os blocos de captura
ProvisionedThroughputExceededException— excluídos, não substituídos - Note toda a lógica de travessia
BlockType.TABLE,BlockType.CELL,BlockType.KEY_VALUE_SET— cada uma precisa de substituição de saída estruturada
Migração de código
- Remova
AWSSDK.Textract,AWSSDK.S3, eAWSSDK.Corede todos os arquivos.csproj - Execute
dotnet add package IronOcrem cada projeto que executa OCR - Adicione
IronOcr.License.LicenseKey = ...uma vez no início do aplicativo (Program.csou equivalente) - Remova todas as declarações
using Amazon.Textract;,using Amazon.Textract.Model;,using Amazon.S3;,using Amazon.Runtime; - Adicione
using IronOcr;para cada arquivo que anteriormente importou namespaces Textract - Substitua chamadas de construtor
AmazonTextractClientcomnew IronTesseract() - Substitua a instanciação
AmazonS3Cliente todas as chamadasPutObjectAsync/DeleteObjectAsynccom leituras diretas de caminho de arquivo ou fluxo - Substitua todas as chamadas
DetectDocumentTextAsync:var text = ocr.Read(path).Text - Substitua todos os loops de polling de
StartDocumentTextDetectionAsync+ cominput.LoadPdf(path)+ocr.Read(input) - Substitua todos os pipelines TIFF de múltiplos frames (TIFF → PDF → S3 → async) com
input.LoadImageFrames(tiffPath) - Substitua cadeias de filtro
BlockType.LINE/BlockType.WORDcomresult.Lines/result.Words - Substitua a travessia de relacionamento
BlockType.KEY_VALUE_SETcom extração de zonaCropRectangle - Exclua todos os blocos de captura
ProvisionedThroughputExceededExceptioneSemaphoreSlimdas limitações - Substitua padrões de lote limitados com
Parallel.ForEachusando uma instânciaIronTesseractcompartilhada - Remova todas as configurações de variáveis de ambiente de credenciais da AWS dos manifestos de implantação e pipelines de CI.
- Desative os buckets de preparação do S3 após a migração e verificação de todo o código de processamento.
Pós-migração
- Verifique se o aplicativo inicia limpo com apenas
IRONOCR_LICENSEdefinido e todas as variáveis de ambiente AWS removidas - Execute o OCR nas mesmas imagens de amostra previamente processadas pelo Textract e compare o texto extraído para verificar a precisão.
- Processar um PDF com várias páginas diretamente e verificar se todas as páginas foram extraídas sem usar S3 ou sondagem assíncrona.
- Processar um TIFF com vários quadros e verificar se a contagem de páginas corresponde à saída do Textract para o mesmo documento.
- Teste o processamento em lote com um conjunto de 50+ imagens e confirme que não ocorre equivalente
ProvisionedThroughputExceededExceptionExecute o aplicativo em um contêiner Docker sem acesso à internet e confirme se o OCR foi concluído com sucesso. - Verifique se o PDF pesquisável abre corretamente no Adobe Reader e se permite pesquisa de texto completo.
- Confirme que remover
AWS_ACCESS_KEY_IDeAWS_SECRET_ACCESS_KEYdo ambiente de implantação não quebra nada - Teste qualquer fluxo de trabalho de extração de formulários ou faturas comparando-o com a saída conhecida do Textract para validar a precisão dos campos.
- Medir a latência de processamento para um conjunto de documentos representativo e confirmar a eliminação da espera mínima de 5 segundos entre as consultas.
Principais benefícios da migração para o IronOCR
Consolidação da infraestrutura em um único pacote NuGet . Três pacotes do AWS SDK, um bucket do S3 com políticas de ciclo de vida, funções do IAM em todos os ambientes de implantação e procedimentos de rotação de credenciais da AWS são substituídos por um único pacote NuGet . A mudança .csproj é dotnet remove package AWSSDK.Textract seguida por dotnet add package IronOcr. Todas as consequências subsequentes do gerenciamento de credenciais da AWS — auditorias de segurança, cronogramas de rotação, higienização de variáveis de ambiente, configuração de segredos do Docker — desaparecem com ele.
Custo Total de Propriedade Previsível. O modelo de cobrança por página gera custos variáveis que aumentam indefinidamente com o volume de documentos. Após a migração para o IronOCR, o custo por página adicional processada é zero, independentemente do volume. Uma equipe processando 200.000 páginas por mês na taxa AnalyzeDocument do Textract para tabelas gasta $36.000 por ano apenas com OCR. A licença IronOCR Enterprise , a US$ 2.999, recupera esse custo em menos de 31 dias de faturamento evitado. Consulte a página de licenciamento do IronOCR para obter detalhes completos sobre os níveis de licenciamento e os termos de redistribuição do SaaS.
O processamento síncrono elimina a complexidade assíncrona de cinco fases. O upload para o S3, o início da tarefa, o loop de polling, a coleta de resultados paginados e o bloco de limpeza final representam cinco modos de falha independentes no pipeline de PDF do Textract. Após a migração, um PDF é lido em duas linhas. O tratamento de erro é reduzido a uma única tentativa/captura em torno da chamada ocr.Read(). A lógica de tempo limite do polling, o loop do/while JobStatus == IN_PROGRESS, o acumulador de paginação nextToken — nenhum desses conceitos existe na base de código do IronOCR. A carga de manutenção diminui proporcionalmente ao código que foi removido.
Flexibilidade total de implantação. O Textract requer acesso à internet em todas as chamadas de OCR. O IronOCR requer acesso à internet apenas para baixar o pacote NuGet durante a instalação. Depois disso, ele opera em redes isoladas (air-gapped), contêineres Docker sem regras de saída, servidores locais e instâncias AWS EC2 sem acesso ao Textract. O mesmo binário que roda em produção no Windows Server também roda em um contêiner Linux. O guia de implantação do Docker e o guia de implantação do Linux abordam a configuração específica para ambientes conteinerizados que anteriormente eram impedidos de usar o Textract.
Soberania de dados alcançada por meio da arquitetura, não da configuração. O IronOCR não possui um modo de transmissão de rede que possa ser desativado — o processamento local é o único modo disponível. Os documentos processados pelo IronOCR nunca saem da máquina host. Para aplicações de saúde que processam PHI, aplicações de defesa que processam CUI e aplicações financeiras que processam imagens de cartões de pagamento, essa postura de conformidade não exige negociação de BAA, configuração de residência de dados regional nem auditoria das políticas de tratamento de dados da Amazon. O âmbito de conformidade se limita à sua própria infraestrutura, que você já controla. A página do produto IronOCR fornece um resumo completo dos recursos e a documentação de implantação para equipes que realizam uma revisão de conformidade.
Pré-processamento configurável para controle de qualidade de documentos. O pré-processamento interno do Textract não é acessível nem configurável. Quando um documento digitalizado produz resultados insatisfatórios, a única solução é aceitar o resultado ou digitalizar novamente.IronOCR expõe todo o pipeline de pré-processamento: Deskew(), DeNoise(), Contrast(), Binarize(), Sharpen(), Scale(), Dilate(), Erode(), e DeepCleanBackgroundNoise(). As equipes que migraram documentos do Textract devido a resultados de baixa confiabilidade em digitalizações complexas podem agora abordar a causa raiz diretamente com filtros específicos para cada defeito — rotação, ruído, baixo contraste ou resolução insuficiente. A página sobre recursos de pré-processamento e o guia de correção da qualidade da imagem documentam os filtros disponíveis e seus respectivos casos de uso.
Perguntas frequentes
Por que devo migrar do Amazon Textract para o IronOCR?
Entre os principais motivos, incluem-se a eliminação da complexidade da interoperabilidade COM, a substituição do gerenciamento de licenças baseado em arquivos, a eliminação da cobrança por página, a viabilização da implantação em Docker/contêineres e a adoção de um fluxo de trabalho nativo do NuGet que se integra às ferramentas padrão do .NET.
Quais são as principais alterações de código ao migrar do Amazon Textract para o IronOCR?
Substitua as sequências de inicialização do Amazon Textract pela instanciação do IronTesseract, remova o gerenciamento do ciclo de vida COM (padrões explícitos de Criação/Carregamento/Fechamento) e atualize os nomes das propriedades de resultado. O resultado é uma redução significativa no número de linhas de código repetitivo.
Como faço para instalar o IronOCR para iniciar a migração?
Execute 'Install-Package IronOcr' no Console do Gerenciador de Pacotes ou 'dotnet add package IronOcr' na CLI. Os pacotes de idiomas são pacotes separados: 'dotnet add package IronOcr.Languages.French' para francês, por exemplo.
O IronOCR tem a mesma precisão de OCR que o Amazon Textract para documentos comerciais padrão?
O IronOCR alcança alta precisão para conteúdo comercial padrão, incluindo faturas, contratos, recibos e formulários digitados. Filtros de pré-processamento de imagem (correção de distorção, remoção de ruído, aprimoramento de contraste) melhoram ainda mais o reconhecimento em entradas de baixa qualidade.
Como o IronOCR lida com os dados de idioma que o Amazon Textract instala separadamente?
Os dados de idioma no IronOCR são distribuídos como pacotes NuGet. O comando 'dotnet add package IronOcr.Languages.German' instala o suporte ao alemão. Não é necessário inserir arquivos manualmente nem configurar caminhos de diretório.
A migração do Amazon Textract para o IronOCR exige alterações na infraestrutura de implantação?
O IronOCR requer menos alterações de infraestrutura do que o Amazon Textract. Não há caminhos binários do SDK, necessidade de instalar arquivos de licença ou configurar servidores de licença. O pacote NuGet contém o mecanismo OCR completo e a chave de licença é uma string definida no código do aplicativo.
Como configuro o licenciamento do IronOCR após a migração?
Atribua `IronOcr.License.LicenseKey = "YOUR-KEY"` no código de inicialização do aplicativo. No Docker ou Kubernetes, armazene a chave como uma variável de ambiente e leia-a na inicialização. Use `License.IsValidLicense` para validar a licença antes de aceitar o tráfego.
O IronOCR consegue processar PDFs da mesma forma que o Amazon Textract?
Sim. O IronOCR lê PDFs nativos e digitalizados. Instancie o IronTesseract, chame ocr.Read(input) onde input é um caminho para um PDF ou OcrPdfInput, e itere pelas páginas do OcrResult. Não é necessário um pipeline de renderização de PDF separado.
Como o IronOCR lida com multithreading em processamento de alto volume?
O IronTesseract pode ser instanciado com segurança por thread. Crie uma instância por thread em um Parallel.ForEach ou pool de Tasks, execute o OCR simultaneamente e descarte cada instância ao terminar. Não é necessário nenhum estado global ou bloqueio.
Quais formatos de saída o IronOCR suporta após a extração de texto?
O IronOCR retorna resultados estruturados, incluindo texto, coordenadas de palavras, níveis de confiança e estrutura da página. As opções de exportação incluem texto simples, PDF pesquisável e objetos de resultados estruturados para processamento posterior.
O preço do IronOCR é mais previsível do que o do Amazon Textract para cargas de trabalho escaláveis?
O IronOCR utiliza licenciamento perpétuo com preço fixo, sem cobranças por página ou volume. Independentemente de você processar 10.000 ou 10 milhões de páginas, o custo da licença permanece constante. As opções de licenciamento por volume e para equipes estão disponíveis na página de preços do IronOCR.
O que acontece com meus testes existentes após a migração do Amazon Textract para o IronOCR?
Os testes que verificam o conteúdo de texto extraído devem continuar a ser aprovados após a migração. Os testes que validam padrões de chamadas de API ou o ciclo de vida de objetos COM precisarão ser atualizados para refletir o modelo de inicialização e resultados mais simples do IronOCR.

