Migración de AWS Textract a IronOCR
Esta guía describe el proceso completo de migración de Texto de AWS a IronOCR para desarrolladores .NET que necesitan procesar documentos localmente sin depender de la nube. Abarca la eliminación de credenciales, la eliminación de la canalización asíncrona, la eliminación de S3 y las sustituciones de código específicas necesarias para trasladar cada operación de Textract a su equivalente en IronOCR.
¿Por qué migrar desde AWS Textract?
AWS Textract es un servicio gestionado muy capaz, pero su arquitectura impone costes, tanto financieros como operativos, que aumentan a medida que crece la carga de trabajo con sus documentos. Los equipos que desarrollan sistemas complejos de procesamiento de documentos tarde o temprano se topan con todos estos puntos de fricción.
La infraestructura de credenciales de AWS complica cada implementación. Cada entorno que ejecuta código de Textract necesita credenciales de AWS: usuario o rol de IAM, clave y secreto de acceso, configuración de región y, para el procesamiento de PDF, permisos de bucket de S3. Esto implica cinco pasos de configuración distintos antes de que se procese la primera página. Las implementaciones en producción requieren políticas de rotación de credenciales, inyección segura en contenedores Docker o pods de Kubernetes, y supervisión para AccessDeniedException cuando la desviación de política IAM causa fallos silenciosos.IronOCR requiere una sola cadena de caracteres: una clave de licencia, que se configura una sola vez al iniciar el sistema.
La fijación de precios por página no tiene techo. La tarifa base de $0.0015 por página para DetectDocumentText es el piso, no el costo. Cualquier documento con tablas activa AnalyzeDocument a $0.015 por página — diez veces la tarifa base. Los formularios añaden 0,05 dólares por página. Un flujo de trabajo con documentos mixtos, con un volumen de 100.000 páginas al mes y extracción de tablas, cuesta 18.000 dólares al año, y esa cifra se reinicia cada enero. La licencia IronOCR Professional tiene un coste único de 2.999 dólares. A partir de ese momento, el coste por página es cero, independientemente del volumen.
El pipeline asincrónico de PDF es una responsabilidad de mantenimiento. Procesar cualquier documento PDF de varias páginas a través de Textract requiere cinco fases distintas: carga en S3, StartDocumentTextDetection, bucle de sondeo, recuperación de resultados paginados, y limpieza de S3. Cada fase es un modo de fallo independiente que requiere su propio manejo de errores, estrategia de reintentos, y gestión de tiempos de espera. Los equipos que implementan este sistema en producción informan sistemáticamente que dedican más tiempo a su mantenimiento que a su desarrollo.IronOCR lee un PDF con solo dos líneas de código.
Sin acceso a Internet, no hay Textract. Los contenedores Docker en segmentos de red aislados, los servidores locales, los entornos de investigación aislados de la red y los sistemas industriales con restricciones de tráfico saliente comparten una característica: Textract no está disponible.IronOCR se instala como un paquete NuGet y funciona sin necesidad de realizar llamadas de red tras la descarga inicial del paquete.
Los datos salen de su infraestructura con cada llamada. Cada operación de OCR con Textract transmite el contenido del documento a los servidores de Amazon. Para las organizaciones sanitarias que procesan información sanitaria protegida (PHI), los contratistas de defensa que manejan información controlada no clasificada (CUI), los equipos legales que procesan comunicaciones privilegiadas y las instituciones financieras que procesan imágenes de tarjetas de pago, esto no es una opción de configuración, sino una restricción arquitectónica que prohíbe por completo el uso de Textract en entornos regulados. Los procesos de IronOCR se ejecutan en su hardware. No hay modo nube para desactivar; El procesamiento local es el único modo de funcionamiento.
Los límites de tasa restringen el rendimiento por lotes. El límite de TPS predeterminado de StartDocumentTextDetection es de 5 solicitudes por segundo. Los trabajos por lotes que procesan cientos de documentos requieren SemaphoreSlim limitación, retrocesos exponenciales en ProvisionedThroughputExceededException, y temporizadores de reabastecimiento de tasa. Para solicitar un aumento de TPS se requiere abrir un caso formal de soporte de AWS, sin garantía de resultado.IronOCR procesa tan rápido como lo permite la CPU local — un servidor de 16 núcleos procesa 16 documentos simultáneamente con un simple Parallel.ForEach, sin necesidad de negociación de nivel de servicio.
El problema fundamental
Cada despliegue de Textract comienza con esta ceremonia, antes de que se pueda procesar una sola página:
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call
// Environment must have:
// AWS_ACCESS_KEY_ID=AKIA...
// AWS_SECRET_ACCESS_KEY=...
// AWS_DEFAULT_REGION=us-east-1
// IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
// IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
// S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
public class TextractOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client; // required for PDFs
public TextractOcrService()
{
// Fails with AmazonClientException if credentials not found in chain
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
}
}
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call
// Environment must have:
// AWS_ACCESS_KEY_ID=AKIA...
// AWS_SECRET_ACCESS_KEY=...
// AWS_DEFAULT_REGION=us-east-1
// IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
// IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
// S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
public class TextractOcrService
{
private readonly AmazonTextractClient _textractClient;
private readonly AmazonS3Client _s3Client; // required for PDFs
public TextractOcrService()
{
// Fails with AmazonClientException if credentials not found in chain
_textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
}
}
Imports Amazon
Imports Amazon.Textract
Imports Amazon.S3
' Textract: five environment variables, an IAM role, and an S3 bucket
' — all required before the first OCR call
' Environment must have:
' AWS_ACCESS_KEY_ID=AKIA...
' AWS_SECRET_ACCESS_KEY=...
' AWS_DEFAULT_REGION=us-east-1
' IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
' IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
' S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)
Public Class TextractOcrService
Private ReadOnly _textractClient As AmazonTextractClient
Private ReadOnly _s3Client As AmazonS3Client ' required for PDFs
Public Sub New()
' Fails with AmazonClientException if credentials not found in chain
_textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
_s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
End Sub
End Class
IronOCR reemplaza toda la cadena de credenciales con una única asignación:
// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
// IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY";
// Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
Imports System
' IronOCR: one line, one string, done
IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"
' Or from environment (no IAM, no rotation, no S3)
IronOcr.License.LicenseKey = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
IronOCR vs AWS Textract: Comparación de características
La siguiente tabla muestra las capacidades de ambas bibliotecas en las dimensiones más relevantes para las decisiones de migración.
| Característica | Texto de AWS | IronOCR |
|---|---|---|
| Lugar de procesamiento | Amazon Cloud (obligatorio) | Solo para consumo local/en el establecimiento. |
| Se requiere Internet | Siempre | Nunca |
| Configuración de credenciales | Usuario/rol de IAM + bucket S3 opcional | Cadena de clave de licencia única |
| PDF de varias páginas | Requiere entorno de preparación S3 + trabajo asíncrono | Inyección sincrónica directa input.LoadPdf() |
| PDF protegido con contraseña | No soportado | input.LoadPdf(path, Password: "x") |
| TIFF multifotograma | No soportado | input.LoadImageFrames("multi.tiff") |
| Se requiere sondeo asíncrono. | Sí (para todos los PDF) | No — síncrono por defecto |
| Límites de tarifa | 5 TPS por defecto (StartDocumentTextDetection) | Ninguno — solo depende de la CPU |
| Costo por página | 0,0015–0,065 $ por página | $0 después de la compra de la licencia |
| Preprocesamiento automático | Interno, no configurable. | Corregir inclinación, Reducir ruido, Contraste, Binarizar, Enfocar, Escalar |
| Salida en PDF con capacidad de búsqueda | No disponible | result.SaveAsSearchablePdf() |
| Exportación hOCR | No disponible | result.SaveAsHocrFile() |
| Lectura de códigos de barras | No disponible | ocr.Configuration.ReadBarCodes = true |
| OCR basado en regiones | A través de AnalyzeDocument + relaciones de bloques | CropRectangle(x, y, width, height) |
| Resultados estructurados | Plano List<Block> filtrado por BlockType |
Tipado Pages, Paragraphs, Lines, Words |
| Idiomas compatibles | Dominante del inglés (seleccione opciones adicionales) | Más de 125 paquetes de idiomas a través de NuGet. |
| Multilingüe simultáneo | No soportado | OcrLanguage.French + OcrLanguage.German |
| Implementación aislada de la red | No es posible | Totalmente compatible |
| HIPAA sin BAA | No es posible | Sin procesador externo: solo para instalación local. |
| Despliegue de Docker | Requiere credenciales de AWS inyectadas | Sin credenciales: paquete NuGet simple |
| Traducción multiplataforma | Gestionado por AWS (solo Linux) | Windows, Linux, macOS, Docker, Azure, AWS EC2 |
| Modelo de licencia | Facturación por página (gasto perpetuo) | Perpetuo de una sola vez ($999 / $1,499 / $2,999) |
Inicio rápido: Migración de Texto de AWS a IronOCR
Paso 1: Sustituir el paquete NuGet
Elimine los paquetes del SDK de 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
Instala IronOCR desde NuGet :
dotnet add package IronOcr
Paso 2: Actualizar los espacios de nombres
Reemplace todas las importaciones de espacios de nombres de AWS con el único espacio de nombres de 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
Paso 3: Inicializar licencia
Agregue la inicialización de la licencia una sola vez al iniciar la aplicación. Elimine toda la configuración de variables de entorno de credenciales de 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"
Ejemplos de migración de código
Reemplazo del constructor AmazonTextractClient y configuración de IAM
El cambio más visible en la migración es la inicialización del cliente. Textract requiere un cliente con inyección de dependencias y resolución de credenciales subyacente, lo que significa que tanto el cliente como todas sus dependencias deben estar preconfigurados en cada entorno de implementación. Cualquier desconfiguración falla silenciosamente hasta que la primera llamada a OCR lanza AmazonClientException.
Enfoque de 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
Enfoque 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
La infraestructura completa de credenciales de AWS — roles de IAM, variables de entorno, archivos ~/.aws/credentials, políticas de ciclo de vida de cubos S3, y configuración de región — se elimina. El constructor DocumentOcrService pasa de un sitio de inyección de dependencia de 3 a una asignación de licencia única con modos de falla en tiempo de ejecución. Para obtener más detalles sobre los patrones de implementación, consulte la guía de configuración de IronTesseract .
Sustitución de StartDocumentTextDetection por el procesamiento de múltiples fotogramas TIFF
La API de trabajo asincrónico de Textract (StartDocumentTextDetection) es necesaria para cualquier documento multipágina. Un patrón común en los procesos de digitalización de documentos es la recepción de documentos escaneados como archivos TIFF multifotograma, es decir, un fotograma por página escaneada. Textract no puede aceptar archivos TIFF de entrada directamente; El documento debe convertirse a formato PDF y subirse a S3 antes de que pueda comenzar un trabajo.IronOCR lee archivos TIFF multifotograma de forma nativa.
Enfoque de 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
Enfoque 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
Se eliminan el paso de conversión de TIFF a PDF, la carga a S3, el bucle de sondeo asíncrono, la acumulación de resultados paginados y la limpieza de S3. La implementación de IronOCR lee los marcos directamente y proporciona acceso por página a través de result.Pages sin ninguna conversión de formato intermedio. La guía de entrada de archivos TIFF y GIF abarca la selección de fotogramas para los casos en los que solo se necesita un subconjunto de fotogramas.
Sustitución del almacenamiento temporal S3 por una salida PDF con capacidad de búsqueda.
Un caso de uso común de Textract es la conversión de archivos PDF escaneados a un formato que permita realizar búsquedas. El método Textract requiere subir cada PDF a S3, ejecutar la tarea asíncrona, recopilar el texto extraído y, a continuación, utilizar una biblioteca PDF independiente para incrustar ese texto como una capa que permita realizar búsquedas.IronOCR realiza el escaneo y genera un PDF con capacidad de búsqueda en una sola llamada.
Enfoque de 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
Enfoque 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
Se han eliminado la lógica de tiempo de espera de sondeo, el patrón de carga y limpieza de S3 y la dependencia de la biblioteca PDF externa. SaveAsSearchablePdf incrusta el texto reconocido directamente en el archivo de salida, haciendo que cada página escaneada sea completamente buscable por texto sin una segunda biblioteca. La guía en formato PDF, que permite realizar búsquedas, abarca la selección de páginas, las opciones de compresión y la inserción de metadatos. Para obtener información más general sobre los procesos de OCR de PDF, consulte la guía de entrada de PDF .
Sustitución del recorrido del grafo de bloques AnalyzeDocument por resultados de página estructurados
El AnalyzeDocument de Textract con TABLES y FORMS devuelve un List<Block> plano donde cada elemento — líneas, palabras, celdas, encabezados de tabla, claves de formulario, valores de formulario — es del mismo tipo Block distinguido solo por BlockType y vinculado por matrices de ID RelationshipType.CHILD. Reconstruir la estructura lógica de un documento requiere recorrer este grafo de relaciones.IronOCR devuelve una jerarquía tipificada: páginas, párrafos, líneas, palabras, cada una con acceso mediante coordenadas.
Enfoque de 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
Enfoque 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
Párrafos, líneas y palabras son directamente accesibles como colecciones tipadas con propiedades .X, .Y, .Width, .Height, y .Confidence. Sin normalización de cuadro delimitador, sin heurísticas de umbral de brecha, sin filtrado de tipo de bloque. La guía de lectura de resultados documenta toda la jerarquía OcrResult incluyendo el acceso a coordenadas a nivel de caracter. Para flujos de trabajo de facturas y recibos que dependen de esta estructura, consulte el tutorial de OCR de facturas .
Sustitución del procesamiento por lotes con limitación de velocidad por ejecución paralela sin restricciones
El procesamiento de imágenes por lotes es donde los límites de TPS de Textract producen el coste operativo más visible. La API sincrónica DetectDocumentText tiene límite de tasa; enviar más de 5 solicitudes por segundo produce ProvisionedThroughputExceededException. El manejo correcto por lotes requiere SemaphoreSlim limitación, lógica de reintentos con retroceso, y una cuidadosa contabilidad de las solicitudes en curso.IronOCR no tiene límite de velocidad; el rendimiento solo está limitado por los núcleos de CPU disponibles.
Enfoque de 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
Enfoque 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
El SemaphoreSlim, el bucle de reintentos, el retroceso exponencial, y los bloques de captura ProvisionedThroughputExceededException se eliminan. IronTesseract es seguro para hilos y una única instancia compartida entre hilos maneja toda la carga paralela sin bloqueos. En una máquina con 8 núcleos, esto procesa 8 documentos simultáneamente sin ninguna configuración; en 32 núcleos, 32 simultáneamente. El ejemplo de multihilo muestra el patrón completo y la guía de optimización de velocidad abarca el ajuste de la configuración del motor para implementaciones centradas en el rendimiento.
Sustitución de la extracción de formularios de AnalyzeDocument por OCR de recorte basado en regiones
El AnalyzeDocument de Textract con FORMS devuelve bloques KEY_VALUE_SET vinculados por relaciones RelationshipType.VALUE. Reconstruir pares clave-valor de campos de formulario requiere filtrar por EntityTypes.Contains("KEY"), seguir IDs de relación VALUE, luego obtener bloques hijos WORD para ensamblar el texto. Para formularios de formato fijo donde las posiciones de los campos son conocidas, el CropRectangle de IronOCR extrae cada campo directamente sin recorrido de grafo de relaciones — y no cuesta nada por página.
Enfoque de 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
Enfoque 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
La filtración de bloques KEY_VALUE_SET, el recorrido RelationshipType.VALUE, y el ayudante GetTextFromBlock se eliminan. Cada campo lee exactamente la región de píxeles que lo contiene, ni más ni menos. La guía de OCR basado en región cubre las coordenadas CropRectangle y cómo definir zonas a partir de mediciones de plantillas. Para flujos de trabajo específicos de facturas, la publicación del blog sobre OCR de facturas y el tutorial de escaneo de recibos muestran procesos completos de extracción de campos.
Referencia de mapeo de la API de Texto de AWS a IronOCR
| Texto de AWS | Equivalente a IronOCR |
|---|---|
AmazonTextractClient |
IronTesseract |
AmazonS3Client |
No es necesario |
DetectDocumentTextRequest |
OcrInput con input.LoadImage() |
DetectDocumentTextResponse |
OcrResult |
AnalyzeDocumentRequest (TABLES/FORMS) |
OcrInput con CropRectangle opcional |
StartDocumentTextDetectionRequest |
OcrInput con input.LoadPdf() — sincrónico, sin inicio de trabajo |
GetDocumentTextDetectionRequest |
No aplica; los resultados son inmediatos. |
Document.Bytes |
input.LoadImage(byteArray) o input.LoadImage(stream) |
S3Object (preparación de documentos) |
Ruta de archivo o flujo: no se requiere área de preparación. |
Block (BlockType.LINE) |
result.Lines (colección tipada) |
Block (BlockType.WORD) |
result.Words (colección tipada) |
Block (BlockType.TABLE) |
result.Words agrupado por proximidad de coordenadas |
Block (BlockType.KEY_VALUE_SET) |
CropRectangle extracción de región por campo |
Block.Confidence |
word.Confidence / result.Confidence |
Block.Geometry.BoundingBox |
word.X, word.Y, word.Width, word.Height |
RelationshipType.CHILD recorrido |
Acceso directo a propiedades en objetos de resultado tipados |
JobStatus.IN_PROGRESS |
No aplicable — sin estado asíncrono |
JobStatus.SUCCEEDED |
No aplicable — retorno sincrónico |
response.NextToken (paginación) |
No aplicable — resultados no paginados |
ProvisionedThroughputExceededException |
No aplicable — sin límites de TPS |
AccessDeniedException |
No aplica: no hay cadena de credenciales |
input.LoadImageFrames() |
Compatibilidad directa con TIFF multifotograma (sin equivalente en Textract) |
result.SaveAsSearchablePdf() |
Salida PDF con capacidad de búsqueda (sin equivalente en Textract) |
Problemas comunes de migración y soluciones
Problema 1: Credenciales mal configuradas en nuevos entornos de implementación.
AWS Textract: El constructor AmazonTextractClient resuelve credenciales a partir de una cadena: variables de entorno, ~/.aws/credentials, perfil de instancia EC2, rol de tarea ECS. En un nuevo contenedor Docker o entorno CI, si ninguno de estos está configurado, el cliente se construye sin errores, pero cada llamada a la API lanza AmazonClientException: No credentials specified. El fallo es invisible hasta el momento de la ejecución.
Solución: Eliminar por completo la cadena de credenciales. Establezca la clave de licencia de IronOCR a partir de una variable de entorno que sea más sencilla de configurar y que no requiera permisos de IAM para su gestión:
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
Imports System
' Single environment variable — no IAM, no rotation, no chain resolution
Dim key As String = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
If String.IsNullOrEmpty(key) Then
Throw New InvalidOperationException("IRONOCR_LICENSE environment variable is not set")
End If
IronOcr.License.LicenseKey = key
Problema 2: Puntos de llamadas asíncronas en todo el código fuente
AWS Textract: Debido a que todo el SDK es solo asincrónico (DetectDocumentTextAsync, StartDocumentTextDetectionAsync, GetDocumentTextDetectionAsync), todos los sitios de llamadas Textract propagan async Task<t> a través de la pila de llamadas. La migración a la API síncrona de IronOCR requiere una decisión en cada sitio.
Solución: Para los servicios de procesamiento en segundo plano y las acciones del controlador, las llamadas síncronas de IronOCR son seguras en los subprocesos en segundo plano. Envoltura en Task.Run solo cuando la cadena de llamadas existente debe permanecer asincrónica:
// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
// Offload synchronous OCR to thread pool — keeps controller non-blocking
var text = await Task.Run(() =>
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage(ms.ToArray());
return ocr.Read(input).Text;
});
return Ok(text);
}
// If the call site must remain async (e.g., an ASP.NET controller action):
public async Task<IActionResult> ProcessUpload(IFormFile file)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
// Offload synchronous OCR to thread pool — keeps controller non-blocking
var text = await Task.Run(() =>
{
var ocr = new IronTesseract();
using var input = new OcrInput();
input.LoadImage(ms.ToArray());
return ocr.Read(input).Text;
});
return Ok(text);
}
Imports System.IO
Imports System.Threading.Tasks
Imports Microsoft.AspNetCore.Mvc
Public Class YourController
Inherits Controller
' If the call site must remain async (e.g., an ASP.NET controller action):
Public Async Function ProcessUpload(file As IFormFile) As Task(Of IActionResult)
Using ms As New MemoryStream()
Await file.CopyToAsync(ms)
' Offload synchronous OCR to thread pool — keeps controller non-blocking
Dim text = Await Task.Run(Function()
Dim ocr = New IronTesseract()
Using input As New OcrInput()
input.LoadImage(ms.ToArray())
Return ocr.Read(input).Text
End Using
End Function)
Return Ok(text)
End Using
End Function
End Class
Para procesadores por lotes y servicios en segundo plano que ya están funcionando en hilos no-UI, llama a ocr.Read() sincrónicamente — Task.Run agrega sobrecarga sin beneficio en esos contextos. La guía de OCR asíncrono abarca los patrones recomendados.
Problema 3: La lógica de limpieza del bucket de S3 está dispersa por todo el código.
AWS Textract: Cualquier código que se cargue en S3 para su procesamiento con Textract también debe eliminarse de S3 una vez que finalice el trabajo. Esta limpieza a menudo vive en bloques finally y a veces se omite en rutas de error, causando objetos huérfanos que acumulan costos de almacenamiento. Durante la migración, es fácil pasar por alto el código de limpieza de S3 porque no está relacionado conceptualmente con el OCR.
Solución: El patrón completo de carga y limpieza de S3 no tiene equivalente en IronOCR. Elimina todos los bloques de PutObjectAsync, DeleteObjectAsync y try/finally relacionados con S3 por completo.IronOCR lee directamente desde rutas o flujos locales:
// 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
Desactive el bucket de almacenamiento temporal de S3 después de la migración. La guía de entrada de PDF y la guía de entrada de flujo cubren todos los patrones de entrada que reemplazan el acceso a documentos por etapas de S3.
Problema 4: El código de recorrido de grafos de bloques no tiene un equivalente directo.
AWS Textract: El código que recorre Block.Relationships para reconstruir celdas de tablas, campos de formularios o agrupaciones de párrafos a partir de matrices de ID RelationshipType.CHILD es el código que más tiempo consume al migrar. No existe una sustitución uno a uno porque IronOCR devuelve colecciones tipificadas en lugar de un gráfico de relaciones.
Solución: Reemplace cada patrón de recorrido con la propiedad tipada apropiada. Las búsquedas de ID de relación se convierten en acceso directo a la propiedad:
// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD &&
childIds.Contains(b.Id))
.Select(b => b.Text));
// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
foreach (var para in page.Paragraphs)
Console.WriteLine(para.Text); // already assembled
// Before: filter by BlockType + traverse relationship IDs
var paragraphText = string.Join(" ", allBlocks
.Where(b => b.BlockType == BlockType.WORD &&
childIds.Contains(b.Id))
.Select(b => b.Text));
// After: direct paragraph text (no filtering, no relationship lookup)
foreach (var page in result.Pages)
foreach (var para in page.Paragraphs)
Console.WriteLine(para.Text); // already assembled
' Before: filter by BlockType + traverse relationship IDs
Dim paragraphText As String = String.Join(" ", allBlocks _
.Where(Function(b) b.BlockType = BlockType.WORD AndAlso _
childIds.Contains(b.Id)) _
.Select(Function(b) b.Text))
' After: direct paragraph text (no filtering, no relationship lookup)
For Each page In result.Pages
For Each para In page.Paragraphs
Console.WriteLine(para.Text) ' already assembled
Next
Next
Para la reconstrucción de tablas que dependía de propiedades BlockType.CELL y ColumnIndex, utiliza agrupación de coordenadas de palabras en result.Words. La guía de lectura de tablas abarca el enfoque basado en coordenadas.
Problema 5: Bloques de captura de excepción ProvisionedThroughputExceededException
AWS Textract: Un código de procesamiento por lotes robusto captura AmazonTextractException con ErrorCode == "ProvisionedThroughputExceededException" e implementa reintentos con retroceso. Este código de error es un concepto específico de Textract que no tiene equivalente en IronOCR.
Solución: Elimina todos los bloques de captura ProvisionedThroughputExceededException.IronOCR no tiene límites de TPS. Reemplaza todo el patrón de lotes con limitación por Parallel.ForEach:
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
var result = new IronTesseract().Read(path);
results[path] = result.Text;
});
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
var result = new IronTesseract().Read(path);
results[path] = result.Text;
});
Imports System.Threading.Tasks
Parallel.ForEach(imagePaths, Sub(path)
Dim result = New IronTesseract().Read(path)
results(path) = result.Text
End Sub)
Problema 6: Configuración de región integrada en los constructores del cliente
AWS Textract: new AmazonTextractClient(Amazon.RegionEndpoint.USEast1) codifica una región. Las implementaciones multirregionales requieren múltiples instancias de cliente o una selección dinámica de región. Cuando Textract añade compatibilidad con nuevas regiones, es necesario actualizar el código.
Solución:IronOCR no tiene concepto de región. Elimina todas las referencias Amazon.RegionEndpoint. El constructor IronTesseract no toma configuración de red — el procesamiento es local. En implementaciones multirregionales en la infraestructura de AWS, donde cada región ejecuta su propio sistema de cómputo, cada instancia de IronOCR procesa los documentos localmente dentro de ese límite de cómputo sin realizar llamadas entre regiones.
Lista de verificación de migración de AWS Textract
Pre-Migración
Auditar todo el uso de Textract y S3 SDK en el código fuente:
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" .
Antes de escribir cualquier código de reemplazo, haga un inventario de lo siguiente:
- Cuenta todos los archivos que contienen
AmazonTextractClient— cada uno es un objetivo de reemplazo - Lista todos los sitios de llamadas
AnalyzeDocument— nota siTABLES,FORMS, o ambos son utilizados - Identifica todos los bucles de sondeo asincrónicos (
do { await Task.Delay } while JobStatus == IN_PROGRESS) - Encuentra todos los bloques de limpieza de S3
finally— estos se eliminan por completo, no se reemplazan - Cuenta todos los bloques de captura
ProvisionedThroughputExceededException— eliminados, no reemplazados - Nota toda la lógica de recorrido
BlockType.TABLE,BlockType.CELL,BlockType.KEY_VALUE_SET— cada uno necesita reemplazo de salida estructurada
Migración de código
- Elimina
AWSSDK.Textract,AWSSDK.S3, yAWSSDK.Corede todos los archivos.csproj - Ejecuta
dotnet add package IronOcren cada proyecto que realiza OCR - Añade
IronOcr.License.LicenseKey = ...una vez al inicio de la aplicación (Program.cso equivalente) - Elimina todas las declaraciones
using Amazon.Textract;,using Amazon.Textract.Model;,using Amazon.S3;,using Amazon.Runtime; - Añade
using IronOcr;a cada archivo que anteriormente importaba espacios de nombres de Textract - Reemplaza las llamadas al constructor
AmazonTextractClientconnew IronTesseract() - Reemplaza la instanciación
AmazonS3Clienty todas las llamadasPutObjectAsync/DeleteObjectAsynccon lecturas de ruta de archivo o flujo directas - Reemplaza todas las llamadas
DetectDocumentTextAsync:var text = ocr.Read(path).Text - Reemplaza todos los bucles de sondeo
StartDocumentTextDetectionAsync+ coninput.LoadPdf(path)+ocr.Read(input) - Reemplaza todas las canalizaciones TIFF de multi-marco (TIFF → PDF → S3 → asincrónico) con
input.LoadImageFrames(tiffPath) - Reemplaza las cadenas de filtros
BlockType.LINE/BlockType.WORDconresult.Lines/result.Words - Reemplaza el recorrido de relaciones
BlockType.KEY_VALUE_SETcon la extracción de zonasCropRectangle - Elimina todos los bloques de captura
ProvisionedThroughputExceededExceptiony limitadoresSemaphoreSlim - Reemplaza los patrones de lotes con limitación por
Parallel.ForEachusando una instancia compartidaIronTesseract - Elimine toda la configuración de variables de entorno de credenciales de AWS de los manifiestos de implementación y las canalizaciones de CI.
- Desactive los buckets de almacenamiento temporal de S3 una vez que se haya migrado y verificado todo el código de procesamiento.
Posmigración
- Verifica que la aplicación arranca limpiamente solo con
IRONOCR_LICENSEconfigurado y todas las variables de entorno de AWS eliminadas - Ejecutar el OCR en las mismas imágenes de muestra procesadas previamente por Textract y comparar el texto extraído para comprobar su precisión.
- Procesar directamente un PDF de varias páginas y verificar que todas las páginas se extraigan sin S3 ni sondeo asíncrono.
- Procesar un archivo TIFF multifotograma y verificar que el número de páginas coincida con la salida de Textract para el mismo documento.
- Prueba el procesamiento por lotes con un conjunto de más de 50 imágenes y confirma que no ocurre un equivalente
ProvisionedThroughputExceededException - Ejecute la aplicación en un contenedor Docker sin acceso a internet saliente y confirme que el OCR se completa correctamente.
- Verificar que el PDF con capacidad de búsqueda se abra correctamente en Adobe Reader y que permita la búsqueda de texto completo.
- Confirma que eliminar
AWS_ACCESS_KEY_IDyAWS_SECRET_ACCESS_KEYdel entorno de implementación no rompe nada - Pruebe cualquier flujo de trabajo de extracción de formularios o facturas comparándolo con la salida conocida de Textract para validar la precisión de los campos.
- Medir la latencia de procesamiento para un conjunto de documentos representativo y confirmar la eliminación del tiempo de espera mínimo de sondeo de 5 segundos.
Principales ventajas de migrar a IronOCR
Consolidación de la infraestructura en un único paquete NuGet . Tres paquetes del SDK de AWS, un bucket de S3 con políticas de ciclo de vida, roles de IAM en todos los entornos de implementación y procedimientos de rotación de credenciales de AWS se reemplazan por un solo paquete NuGet . El cambio de .csproj es dotnet remove package AWSSDK.Textract seguido por dotnet add package IronOcr. Todas las consecuencias derivadas de la gestión de credenciales de AWS (auditorías de seguridad, programas de rotación, higiene de variables de entorno, configuración de secretos de Docker) desaparecen con ella.
Coste total de propiedad predecible. El modelo de facturación por página genera costes variables que aumentan indefinidamente con el volumen de documentos. Tras migrar a IronOCR, el coste por página adicional procesada es cero, independientemente del volumen. Un equipo que procesa 200,000 páginas por mes a la tasa de AnalyzeDocument de Textract para tablas gasta $36,000 por año solo en OCR. La licencia IronOCR Enterprise , con un precio de 2999 dólares, permite recuperar ese coste en menos de 31 días gracias a la reducción de la facturación. Consulte la página de licencias de IronOCR para obtener información detallada sobre los niveles y los términos de redistribución de SaaS.
El procesamiento síncrono elimina la complejidad asíncrona de cinco fases. La carga a S3, el inicio del trabajo, el bucle de sondeo, la recopilación de resultados paginados y el bloque de limpieza final representan cinco modos de fallo independientes en la canalización de PDF de Textract. Tras la migración, un PDF se lee en dos líneas. El manejo de errores se reduce a un solo try/catch alrededor de la llamada ocr.Read(). La lógica de tiempo de espera de sondeo, el bucle do/while JobStatus == IN_PROGRESS, el acumulador de paginación nextToken — ninguno de estos conceptos existe en la base de código de IronOCR. La carga de mantenimiento disminuye en proporción al código que se eliminó.
Flexibilidad total en la implementación. Textract requiere acceso a internet en cada llamada de OCR.IronOCR solo requiere acceso a internet para descargar el paquete NuGet durante la instalación. Posteriormente, funciona en redes aisladas, contenedores Docker sin reglas de salida, servidores locales e instancias AWS EC2 sin acceso a Textract. El mismo archivo binario que se ejecuta en producción en Windows Server también se ejecuta en un contenedor Linux. La guía de implementación de Docker y la guía de implementación de Linux cubren la configuración específica para entornos en contenedores que anteriormente tenían bloqueado el uso de Textract.
La soberanía de los datos se logra mediante la arquitectura, no mediante la configuración.IronOCR no tiene ningún modo de transmisión de red que se pueda desactivar; el procesamiento local es el único modo disponible. Los documentos procesados mediante IronOCR nunca salen del equipo anfitrión. Para las aplicaciones sanitarias que procesan información de salud protegida (PHI), las aplicaciones de defensa que procesan información controlada no clasificada (CUI) y las aplicaciones financieras que procesan imágenes de tarjetas de pago, esta es una solución de cumplimiento que no requiere negociación de acuerdos de asociación comercial (BAA), configuración de residencia de datos regional ni auditoría de las políticas de gestión de datos de Amazon. El ámbito de cumplimiento normativo es el límite de su propia infraestructura, que usted ya controla. La página del producto IronOCR proporciona un resumen completo de las características y la documentación de implementación para los equipos que realizan una revisión de cumplimiento.
Preprocesamiento configurable para el control de calidad de documentos. El preprocesamiento interno de Textract no es accesible ni configurable. Cuando un documento escaneado produce resultados deficientes, la única solución es aceptar el resultado o volver a escanearlo.IronOCR expone toda la canalización de preprocesamiento: Deskew(), DeNoise(), Contrast(), Binarize(), Sharpen(), Scale(), Dilate(), Erode(), y DeepCleanBackgroundNoise(). Los equipos que trasladaron documentos de Textract debido a los resultados poco fiables en escaneos difíciles pueden abordar la causa raíz directamente con filtros que coincidan con el defecto específico: rotación, ruido, bajo contraste o resolución insuficiente. La página de funciones de preprocesamiento y la guía de corrección de la calidad de la imagen documentan los filtros disponibles y sus casos de uso apropiados.
Preguntas Frecuentes
¿Por qué debería migrar de Amazon Textract a IronOCR?
Los impulsores comunes incluyen la eliminación de la complejidad de la interoperabilidad COM, la sustitución de la gestión de licencias basada en archivos, la evitación de la facturación por página, la habilitación de la implementación de Docker/contenedores y la adopción de un flujo de trabajo nativo de NuGet que se integre con las herramientas .NET estándar.
¿Cuáles son los principales cambios de código al migrar de Amazon Textract a IronOCR?
Sustituya las secuencias de inicialización de Amazon Textract por la instanciación de IronTesseract, elimine la gestión del ciclo de vida de COM (patrones Create/Load/Close explícitos) y actualice los nombres de las propiedades de los resultados. El resultado es un número significativamente menor de líneas repetitivas.
¿Cómo instalo IronOCR para comenzar la migración?
Ejecute 'Install-Package IronOcr' en la consola del gestor de paquetes o 'dotnet add package IronOcr' en la CLI. Los paquetes de idiomas son paquetes independientes: 'dotnet add package IronOcr.Languages.French' para el francés, por ejemplo.
¿IronOCR alcanza la precisión de OCR de Amazon Textract para documentos comerciales estándar?
IronOCR consigue una gran precisión para contenido empresarial estándar, como facturas, contratos, recibos y formularios mecanografiados. Los filtros de preprocesamiento de imágenes (eliminación de distorsiones, eliminación de ruido, mejora del contraste) mejoran aún más el reconocimiento en entradas degradadas.
¿Cómo gestiona IronOCR los datos de idioma que Amazon Textract instala por separado?
Los datos de idiomas en IronOCR se distribuyen como paquetes NuGet. dotnet add package IronOcr.Languages.German' instala la compatibilidad con el alemán. No es necesario colocar manualmente los archivos ni las rutas de los directorios.
¿La migración de Amazon Textract a IronOCR requiere cambios en la infraestructura de implementación?
IronOCR requiere menos cambios de infraestructura que Amazon Textract. No hay rutas binarias del SDK, ubicaciones de archivos de licencia ni configuraciones del servidor de licencias. El paquete NuGet contiene el motor OCR completo, y la clave de licencia es una cadena establecida en el código de la aplicación.
¿Cómo configuro las licencias de IronOCR después de la migración?
Asigne IronOcr.License.LicenseKey = "YOUR-KEY" en el código de inicio de la aplicación. En Docker o Kubernetes, almacene la clave como una variable de entorno y léala en el inicio. Utilice License.IsValidLicense para validar antes de aceptar tráfico.
¿Puede IronOCR procesar archivos PDF del mismo modo que Amazon Textract?
Sí, IronOCR lee PDF nativos y escaneados. Instancie IronTesseract, llame a ocr.Read(input) donde input es una ruta PDF u OcrPdfInput, e itere las páginas OcrResult. No es necesario un proceso de renderizado de PDF independiente.
¿Cómo gestiona IronOCR los hilos en el procesamiento de grandes volúmenes?
IronTesseract puede instanciarse de forma segura por subproceso. Gire una instancia por hilo en un Parallel.ForEach o Task pool, ejecute OCR concurrentemente, y disponga de cada instancia cuando termine. No se requiere estado global o bloqueo.
¿Qué formatos de salida admite IronOCR tras la extracción de texto?
IronOCR devuelve resultados estructurados que incluyen texto, coordenadas de palabras, puntuaciones de confianza y estructura de páginas. Las opciones de exportación incluyen texto sin formato, PDF con opción de búsqueda y objetos de resultados estructurados para su procesamiento posterior.
¿Es el precio de IronOCR más predecible que el de Amazon Textract para escalar cargas de trabajo?
IronOCR utiliza licencias perpetuas de tarifa plana sin cargos por página o volumen. Tanto si procesa 10.000 como 10 millones de páginas, el coste de la licencia permanece constante. Las opciones de licencias por volumen y por equipo se encuentran en la página de precios de IronOCR.
¿Qué ocurre con mis pruebas existentes después de migrar de Amazon Textract a IronOCR?
Las pruebas que validan el contenido de texto extraído deben seguir superándose tras la migración. Las pruebas que validan patrones de llamada a API o el ciclo de vida de objetos COM deberán actualizarse para reflejar el modelo de inicialización y resultados más sencillo de IronOCR.

