Ir para o conteúdo do rodapé
VíDEOS

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
$vbLabelText   $csharpLabel

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")
$vbLabelText   $csharpLabel

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
SHELL

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
$vbLabelText   $csharpLabel

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"
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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)
$vbLabelText   $csharpLabel

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" .
SHELL

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 se TABLES, 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

  1. Remova AWSSDK.Textract, AWSSDK.S3, e AWSSDK.Core de todos os arquivos .csproj
  2. Execute dotnet add package IronOcr em cada projeto que executa OCR
  3. Adicione IronOcr.License.LicenseKey = ... uma vez no início do aplicativo (Program.cs ou equivalente)
  4. Remova todas as declarações using Amazon.Textract;, using Amazon.Textract.Model;, using Amazon.S3;, using Amazon.Runtime;
  5. Adicione using IronOcr; para cada arquivo que anteriormente importou namespaces Textract
  6. Substitua chamadas de construtor AmazonTextractClient com new IronTesseract()
  7. Substitua a instanciação AmazonS3Client e todas as chamadas PutObjectAsync / DeleteObjectAsync com leituras diretas de caminho de arquivo ou fluxo
  8. Substitua todas as chamadas DetectDocumentTextAsync: var text = ocr.Read(path).Text
  9. Substitua todos os loops de polling de StartDocumentTextDetectionAsync + com input.LoadPdf(path) + ocr.Read(input)
  10. Substitua todos os pipelines TIFF de múltiplos frames (TIFF → PDF → S3 → async) com input.LoadImageFrames(tiffPath)
  11. Substitua cadeias de filtro BlockType.LINE / BlockType.WORD com result.Lines / result.Words
  12. Substitua a travessia de relacionamento BlockType.KEY_VALUE_SET com extração de zona CropRectangle
  13. Exclua todos os blocos de captura ProvisionedThroughputExceededException e SemaphoreSlim das limitações
  14. Substitua padrões de lote limitados com Parallel.ForEach usando uma instância IronTesseract compartilhada
  15. Remova todas as configurações de variáveis ​​de ambiente de credenciais da AWS dos manifestos de implantação e pipelines de CI.
  16. 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_LICENSE definido 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 ProvisionedThroughputExceededException Execute 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_ID e AWS_SECRET_ACCESS_KEY do 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.

ObserveAWS Textract, PDFSharp, Tesseract, e iText são marcas registradas de seus respectivos proprietários. Este site não é afiliado, endossado ou patrocinado pela Amazon Web Services, Google, empira Software GmbH, ou iText Group. Todos os nomes de produtos, logotipos e marcas são propriedade de seus respectivos proprietários. As comparações são apenas para fins informativos e refletem informações disponíveis publicamente no momento da redação.

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.

Kannaopat Udonpant
Engenheiro de Software
Antes de se tornar Engenheiro de Software, Kannapat concluiu um doutorado em Recursos Ambientais pela Universidade de Hokkaido, no Japão. Durante o doutorado, Kannapat também integrou o Laboratório de Robótica Veicular, que faz parte do Departamento de Engenharia de Bioprodução. Em 2022, ele utilizou suas habilidades ...
Leia mais

Equipe de Suporte Iron

Estamos online 24 horas por dia, 5 dias por semana.
Bater papo
E-mail
Liga para mim