从 AWS Textract 迁移到 IronOCR
本指南将引导.NET开发人员完成从AWS Textract到IronOCR 的完整迁移,以满足他们在本地进行文档处理而无需依赖云的需求。 它涵盖了凭证拆除、异步管道移除、S3 消除,以及将每个 Textract 操作迁移到其IronOCR等效项所需的特定代码替换。
为什么要从AWS Textract迁移?
AWS Textract 是一项功能强大的托管服务,但其架构会带来成本(财务和运营成本),并且随着文档工作负载的增长而增长。 构建严肃的文档处理流程的团队最终都会遇到所有这些摩擦点。
AWS凭证基础设施使每次部署都更加复杂。每个运行Textract代码的环境都需要AWS凭证:IAM用户或角色、访问密钥和密钥、区域配置,以及(对于PDF处理)额外的S3存储桶权限。这意味着在处理第一个页面之前,需要五个不同的配置步骤。 生产部署需要凭证轮换策略、安全注入到Docker容器或Kubernetes pods,以及当IAM策略漂移导致静默失败时监控AccessDeniedException。 IronOCR需要一个字符串:许可证密钥,在启动时设置一次。
每页定价无上限。 针对AnalyzeDocument——是基准费率的十倍。 表格每页额外收费 0.05 美元。 每月处理 10 万页混合文档的工作流程(含表格提取)每年成本为 1.8 万美元,并且该数字每年一月重置。 IronOCRProfessional许可证一次性收费 2,999 美元。 此后,无论页面大小,每页成本均为零。
异步PDF流水线是一个维护责任。 通过Textract处理任何多页PDF需要五个独立阶段:S3上传、StartDocumentTextDetection、轮询循环、分页结果检索和S3清理。每个阶段都是一个独立的故障模式,需具备各自的错误处理、重试策略及超时管理。 将该管道投入生产环境的团队普遍反映,维护它所花费的时间比构建它所花费的时间还要多。 IronOCR只需两行代码即可读取 PDF 文件。
没有互联网连接就无法使用 Textract。隔离网络段中的 Docker 容器、本地服务器、物理隔离的研究环境以及有出站流量限制的工业系统都有一个共同的特点:Textract 不可用。 IronOCR以NuGet包的形式安装,在初始包下载后无需任何网络调用即可运行。
每次调用都会将数据从您的基础设施中传输出去。使用 Textract 进行的每次 OCR 操作都会将文档内容传输到亚马逊的服务器。 对于处理 PHI 的医疗机构、处理 CUI 的国防承包商、处理特权通信的法律团队以及处理支付卡图像的金融机构而言,这不是一个配置选项,而是一个架构限制,它完全禁止在受监管的环境中使用 Textract。 IronOCR在您的硬件上进行处理。 没有可以关闭的云模式; 本地处理是唯一模式。
速率限制约束批处理吞吐量。 默认的StartDocumentTextDetection TPS限制是每秒5个请求。 处理数百个文档的批量作业需要ProvisionedThroughputExceededException的指数退避、以及速率补充计时器。 申请提高 TPS 需要提交正式的 AWS 支持案例,且无法保证结果。 IronOCR处理速度与本地CPU所允许的一样快——16核服务器同时处理16个文档,使用简单的Parallel.ForEach,无需服务层协商。
基本问题
每次 Textract 部署都从这个仪式开始——在处理任何页面之前:
// 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将整个凭证链替换为单个赋值:
// 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与 AWS Textract:功能对比
下表列出了两个库在与迁移决策最相关的维度上的功能。
| 特征 | AWS Textract | IronOCR |
|---|---|---|
| 处理地点 | 亚马逊云(必填) | 仅限本地/现场使用 |
| 需要互联网 | 总是 | 绝不 |
| 凭证设置 | IAM 用户/角色 + 可选的 S3 存储桶 | 单个许可证密钥字符串 |
| 多页PDF | 需要 S3 暂存环境 + 异步作业 | 直接同步input.LoadPdf() |
| 受密码保护的PDF | 不支持 | input.LoadPdf(path, Password: "x") |
| 多帧 TIFF | 不支持 | input.LoadImageFrames("multi.tiff") |
| 需要异步轮询 | 是的(适用于所有PDF文件) | 否——默认情况下是同步的 |
| 费用限制 | 5 TPS 默认值(StartDocumentTextDetection) | 无 — 仅限 CPU 密集型 |
| 每页成本 | 每页 0.0015 美元至 0.065 美元 | 购买许可证后费用为 0 美元 |
| 自动预处理 | 内部,不可配置 | 校正倾斜、降噪、增强对比度、二值化、锐化、缩放 |
| 可搜索的 PDF 输出 | 不可用 | result.SaveAsSearchablePdf() |
| hOCR出口 | 不可用 | result.SaveAsHocrFile() |
| 条形码读取 | 不可用 | ocr.Configuration.ReadBarCodes = true |
| 基于区域的OCR | 通过 AnalyzeDocument + 块关系 | CropRectangle(x, y, width, height) |
| 结构化结果 | 由BlockType |
类型化的Words |
| 支持的语言 | 英语为主导(选择其他) | 通过NuGet提供 125 多个语言包 |
| 多语言同步 | 不支持 | OcrLanguage.French + OcrLanguage.German |
| 气隙部署 | 不可能 | 完全支持 |
| 无需 BAA 即可获得 HIPAA 认证 | 不可能 | 无需外接处理器——仅限本地部署 |
| Docker部署 | 需要注入 AWS 凭证 | 无需凭据 — 普通的NuGet包 |
| 跨平台 | AWS托管(仅限Linux) | Windows、Linux、macOS、Docker、Azure、AWS EC2 |
| 许可模式 | 按页计费(持续消费) | 永久一次性($999 / $1,499 / $2,999) |
快速入门:AWS Textract 到IronOCR 的迁移
步骤 1:替换 NuGet 软件包
移除 AWS SDK 软件包:
dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
dotnet remove package AWSSDK.Textract
dotnet remove package AWSSDK.S3
dotnet remove package AWSSDK.Core
从NuGet安装IronOCR :
dotnet add package IronOcr
步骤 2:更新命名空间
将所有 AWS 命名空间导入替换为单个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
步骤 3:初始化许可证
在应用程序启动时进行一次许可证初始化。移除所有 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"
代码迁移示例
替换 AmazonTextractClient 构造函数和 IAM 设置
迁移过程中最明显的变化是客户端初始化。 Textract 需要一个依赖注入客户端,该客户端具有底层凭据解析功能——这意味着客户端及其所有依赖项都必须在每个部署环境中预先配置。 任何错误配置都会默默失败,直到第一次OCR调用抛出AmazonClientException。
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
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
整个AWS凭证基础设施——IAM角色、环境变量、~/.aws/credentials文件、S3桶生命周期策略和区域配置——被移除。 DocumentOcrService构造函数从具有运行时故障模式的三依赖注入点变为单一许可分配。 有关部署模式的详细信息,请参阅IronTesseract 设置指南。
用 TIFF 多帧处理替换 StartDocumentTextDetection
Textract的异步作业API(StartDocumentTextDetection)是处理任何多页文档所必需的。 文档数字化流程中常见的模式是接收以多帧 TIFF 格式的扫描文档——每帧扫描一页。 Textract 完全无法直接接受 TIFF 输入; 在作业开始之前,必须将文档转换为 PDF 格式并上传到 S3。 IronOCR可直接读取多帧 TIFF 文件。
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
IronOCR方法:
//IronOCRreads 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;
}
//IronOCRreads 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
TIFF 到 PDF 的转换步骤、S3 上传、异步轮询循环、分页结果累积以及 S3 清理都被取消了。 IronOCR实现直接读取帧,通过result.Pages提供每页访问,不需要任何中间格式转换。 TIFF 和 GIF 输入指南涵盖了仅需部分帧时如何选择帧。
用可搜索的 PDF 输出替换 S3 暂存环境
Textract 的一个常见用例是将扫描的 PDF 存档转换为可搜索的形式。 Textract 方法需要将每个 PDF 上传到 S3,运行异步作业,收集提取的文本,然后使用单独的 PDF 库将该文本嵌入为可搜索的图层。 IronOCR一次调用即可完成扫描并生成可搜索的 PDF 文件。
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
IronOCR方法:
//IronOCRreads 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}%");
}
//IronOCRreads 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
轮询超时逻辑、S3 上传和清理模式以及外部 PDF 库依赖项都已移除。 SaveAsSearchablePdf直接将识别的文本嵌入输出文件中,使每个扫描页面都能进行全文本搜索,而无需第二个库。 该可搜索的 PDF 指南涵盖页面选择、压缩选项和元数据嵌入。 有关 PDF OCR 流程的更广泛背景,请参阅PDF 输入指南。
将 AnalyzeDocument 块图遍历替换为结构化页面结果
Textract的RelationshipType.CHILD ID数组连接。 重建文档的逻辑结构需要遍历此关系图。 IronOCR返回一个类型化的层次结构:页面、段落、行、单词,每个都有坐标访问。
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
IronOCR方法:
//IronOCRreturns 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; }
}
//IronOCRreturns 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
段落、行和单词通过.Confidence属性直接以类型化集合形式访问。 没有边界框归一化,没有间隙阈值启发式方法,没有块类型过滤。 读取结果指南记录了完整的OcrResult层级,包括字符级坐标访问。 对于依赖于此结构的发票和收据工作流程,请参阅发票 OCR 教程。
用不受限制的并行执行取代速率受限的批处理
批量图像处理是Textract的TPS限制产生最明显操作成本的地方。DetectDocumentText同步API有速率限制; 每秒发送超过5个请求会导致ProvisionedThroughputExceededException。 正确的批量处理需要SemaphoreSlim节流,具有退避的重试逻辑,以及对在途请求的仔细计算。 IronOCR没有速率限制——吞吐量仅受可用 CPU 核心的限制。
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
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
ProvisionedThroughputExceededException捕获块全都被移除。 IronTesseract是线程安全的,一个共享的实例能在不加锁的情况下处理全部并行负载。 在一台 8 核机器上,无需任何配置即可同时处理 8 个文档; 在 32 个核心上,同时运行 32 个核心。 多线程示例展示了完整的模式,速度优化指南涵盖了针对以吞吐量为中心的部署的引擎配置调整。
用基于区域的 CropRectangle OCR 替换 AnalyzeDocument 表单提取
Textract的KEY_VALUE_SET块。 重建表单字段的键值对需要通过WORD块以组装文本。 对于字段位置已知的固定格式表单,IronOCR的CropRectangle直接提取每个字段,无需遍历关系图——每页成本为零。
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
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
GetTextFromBlock助手被消除。 每个字段都精确读取包含它的像素区域——不多也不少。 基于区域的OCR指南涵盖CropRectangle坐标及如何根据模板测量定义区域。 针对发票相关的工作流程,发票 OCR 博客文章和收据扫描教程演示了完整的字段提取流程。
AWS TextractAPI 到IronOCR映射参考
| AWS Textract | IronOCR当量 |
|---|---|
AmazonTextractClient |
IronTesseract |
AmazonS3Client |
不要求 |
DetectDocumentTextRequest |
input.LoadImage() |
DetectDocumentTextResponse |
OcrResult |
AnalyzeDocumentRequest (表格/表单) |
CropRectangle |
StartDocumentTextDetectionRequest |
input.LoadPdf()——同步,无作业启动 |
GetDocumentTextDetectionRequest |
不适用——结果立竿见影 |
Document.Bytes |
input.LoadImage(stream) |
S3Object(文档暂存) |
文件路径或流——无需暂存 |
BlockType.LINE) |
result.Lines(类型化集合) |
BlockType.WORD) |
result.Words(类型化集合) |
BlockType.TABLE) |
result.Words按坐标接近分组 |
BlockType.KEY_VALUE_SET) |
CropRectangle每个字段的区域提取 |
Block.Confidence |
word.Confidence / result.Confidence |
Block.Geometry.BoundingBox |
word.X, word.Y, word.Width, word.Height |
RelationshipType.CHILD遍历 |
直接访问类型化结果对象的属性 |
JobStatus.IN_PROGRESS |
不适用——无异步状态 |
JobStatus.SUCCEEDED |
不适用 — 同步返回 |
response.NextToken(分页) |
不适用——结果未分页 |
ProvisionedThroughputExceededException |
不适用——无TPS限制 |
AccessDeniedException |
不适用——无凭证链 |
input.LoadImageFrames() |
多帧 TIFF 直接支持(无 Textract 等效项) |
result.SaveAsSearchablePdf() |
可搜索的PDF输出(无Textract等效项) |
常见迁移问题和解决方案
问题一:新部署环境中的凭据配置错误
AWS Textract:~/.aws/credentials、EC2实例配置文件、ECS任务角色。 在新的Docker容器或CI环境中,如果这些都未配置,客户端构造不会报错,但每个API调用都会抛出AmazonClientException: No credentials specified。 该故障在运行时才会显现。
解决方案:彻底删除凭证链。 通过环境变量设置IronOCR许可证密钥,这样配置起来更简单,而且管理起来不需要 IAM 权限:
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
// Single environment variable — no IAM, no rotation, no chain resolution
var key = Environment.GetEnvironmentVariable("IRONOCR_LICENSE");
if (string.IsNullOrEmpty(key))
throw new InvalidOperationException("IRONOCR_LICENSE environment variable is not set");
IronOcr.License.LicenseKey = key;
Imports System
' Single environment variable — no IAM, no rotation, no chain resolution
Dim key As String = Environment.GetEnvironmentVariable("IRONOCR_LICENSE")
If String.IsNullOrEmpty(key) Then
Throw New InvalidOperationException("IRONOCR_LICENSE environment variable is not set")
End If
IronOcr.License.LicenseKey = key
问题 2:代码库中的异步调用点
AWS Textract:因为整个SDK是异步专用的(async Task<t>。 迁移到IronOCR的同步 API 需要每个站点做出决定。
解决方案:对于后台处理服务和控制器操作,IronOCR 的同步调用在后台线程上是安全的。 仅当现有调用链必须保持异步时才包裹在Task.Run中:
// 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
对于已经在非UI线程上运行的批处理器和后台服务,Task.Run增加了开销而无益。 异步OCR指南涵盖了推荐的模式。
问题 3:S3 存储桶清理逻辑分散在代码各处
AWS Textract:任何上传到 S3 进行 Textract 处理的代码,在作业完成后也必须从 S3 中删除。 此清理经常在finally块中进行,有时在错误路径中被省略,导致累积存储成本的孤立对象。 在迁移过程中,S3 清理代码很容易被忽略,因为它在概念上与 OCR 无关。
解决方案:整个 S3 上传和清理模式没有IronOCR等效项。 完全删除所有try/finally块。 IronOCR直接从本地路径或流读取数据:
// 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
问题 4:块图遍历代码没有直接等效代码
AWS Textract:遍历Block.Relationships以重建表格单元格、表单字段或段落分组的代码是迁移中耗时最长的部分。 由于IronOCR返回的是类型化的集合而不是关系图,因此不存在一对一的替换。
解决方案:将每个遍历模式替换为相应的类型化属性。 关系 ID 查找将变为直接属性访问:
// 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
对于依赖于result.Words单词坐标分组来处理。 表格阅读指南涵盖了基于坐标的方法。
问题 5:ProvisionedThroughputExceededException 捕获块
AWS Textract:健壮的批量处理代码通过ErrorCode == "ProvisionedThroughputExceededException"并实现具有退避的重试。 此错误代码是 Textract 特有的概念, IronOCR没有对应的代码。
解决方案:删除所有ProvisionedThroughputExceededException捕获块。 IronOCR没有TPS限制。 用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)
问题 6:将区域配置集成到客户端构造函数中
AWS Textract:new AmazonTextractClient(Amazon.RegionEndpoint.USEast1)硬编码地域。 多区域部署需要多个客户端实例或动态区域选择。 当 Textract 为新地区添加支持时,代码必须更新。
解决方案: IronOCR没有区域概念。 删除所有Amazon.RegionEndpoint引用。 IronTesseract构造函数不需要网络配置——处理是本地的。 对于 AWS 基础设施上的多区域部署,每个区域都运行自己的计算,每个IronOCR实例都在其计算边界内本地处理文档,而不会进行跨区域调用。
AWS Textract迁移清单
迁移前
审核代码库中所有 Textract 和 S3 SDK 的使用情况:
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" .
在编写任何替换代码之前,请清点以下物品:
- 计算所有包含
AmazonTextractClient的文件——每一个都是替换目标 - 列出所有
FORMS或两者兼有 - 确定所有异步轮询循环(
do { await Task.Delay } while JobStatus == IN_PROGRESS) - 找到所有S3清理
finally块——这些是完全删除的,不会被替换 - 计算所有
ProvisionedThroughputExceededException捕获块——删除,不替换 - 记录所有
BlockType.KEY_VALUE_SET遍历逻辑——每个都需要结构化输出替换
代码迁移
- 从所有
AWSSDK.Core - 在每个执行OCR的项目中运行
dotnet add package IronOcr - 在应用程序启动时添加一次
Program.cs或等效) - 删除所有
using Amazon.Runtime;语句 - 向每个以前导入Textract命名空间的文件添加
using IronOcr; - 用
AmazonTextractClient构造函数调用 - 用直接文件路径或流读取替换
DeleteObjectAsync调用 - 替换所有
var text = ocr.Read(path).Text - 用
StartDocumentTextDetectionAsync+轮询循环 - 用
input.LoadImageFrames(tiffPath)替换所有多帧TIFF管道(TIFF→PDF→S3→异步) - 用
BlockType.WORD过滤链 - 用
BlockType.KEY_VALUE_SET关系遍历 - 删除所有
SemaphoreSlim节流 - 用共享的
Parallel.ForEach替换节流的批处理模式 - 从部署清单和 CI 管道中移除所有 AWS 凭证环境变量配置
- 所有处理代码迁移并验证完毕后,停用 S3 暂存存储桶。
后迁移
- 验证仅在设置了
IRONOCR_LICENSE且所有AWS环境变量被移除的情况下,应用程序能清洁启动 - 对之前由 Textract 处理过的相同样本图像运行 OCR,并比较提取的文本的准确性。
- 直接处理多页PDF文件,并验证所有页面是否都已提取,无需使用S3或异步轮询
- 处理多帧 TIFF 文件,并验证页数是否与 Textract 对同一文档的输出结果一致。
- 使用50+张图像集测试批量处理,并确认没有发生
ProvisionedThroughputExceededException等效 - 在无法访问外部互联网的 Docker 容器中运行应用程序,并确认 OCR 功能已成功完成。
- 验证可搜索的 PDF 输出文件是否能在 Adobe Reader 中正确打开,并且支持全文搜索。
- 确认从部署环境中移除
AWS_SECRET_ACCESS_KEY不会导致任何损坏 - 使用 Textract 的已知输出测试任何表单或发票提取工作流程,以验证字段准确性
- 测量代表性文档集的处理延迟,并确认消除至少 5 秒的轮询等待时间
迁移到IronOCR的主要优势
将基础设施整合为一个NuGet包。三个 AWS SDK 包、一个包含生命周期策略的 S3 存储桶、跨所有部署环境的 IAM 角色以及 AWS 凭证轮换流程,都被一个NuGet包所取代。 dotnet add package IronOcr。 AWS凭证管理的所有下游后果——安全审计、轮换计划、环境变量清理、Docker密钥配置——都随之消失了。
可预测的总拥有成本。按页计费模式产生的可变成本会随着文档量的增加而无限增长。 迁移到IronOCR后,无论处理量多大,每增加一页的处理成本都为零。 一个以Textract的AnalyzeDocument费率处理每月20万页的团队,仅OCR的年支出便达36,000美元。 IronOCREnterprise版许可证售价 2,999 美元,可在 31 天内通过避免账单收回成本。 有关完整分级详情和 SaaS 再分发条款,请参阅IronOCR许可页面。
同步处理消除了五阶段异步处理的复杂性。S3上传、作业启动、轮询循环、分页结果收集和最终清理模块代表了 Textract PDF 流水线中的五个独立故障模式。迁移后,PDF 文件以两行读取。 错误处理减少为围绕ocr.Read()调用的单个try/catch。 轮询超时逻辑,nextToken分页累加器——这些概念在IronOCR代码库中都不存在。 维护负担会随着移除代码量的减少而相应降低。
部署灵活性极高。Textract每次 OCR 调用都需要联网。 IronOCR仅在安装时需要联网下载NuGet包。之后,它可以在物理隔离的网络、无出站规则的 Docker 容器、本地服务器以及无需访问 Textract 的 AWS EC2 实例上运行。 在 Windows Server 生产环境中运行的同一个二进制文件也可以在 Linux 容器中运行。 Docker 部署指南和Linux 部署指南涵盖了之前无法使用 Textract 的容器化环境的具体配置。
数据主权是通过架构而非配置实现的。IronOCR没有可禁用的网络IronOCR模式——本地处理是唯一模式。 通过IronOCR处理的文档始终保留在主机上。对于处理 PHI(受保护健康信息)的医疗保健应用、处理 CUI(受控非密信息)的国防应用以及处理支付卡图像的金融应用而言,这种合规方式无需进行 BAA(业务伙伴协议)谈判、无需配置区域数据驻留,也无需审核亚马逊的数据处理策略。 合规范围就是您自己的基础设施边界,这是您已经控制的范围。 IronOCR产品页面为完成合规性审查的团队提供了完整的功能概述和部署文档。
用于文档质量控制的可配置预处理。Textract的内部预处理功能无法访问或配置。 当扫描文档产生不良结果时,唯一的办法是接受输出结果或重新扫描。 IronOCR公开了完整预处理流程:DeepCleanBackgroundNoise()。 由于在处理疑难扫描时结果置信度较低,导致团队将文档从 Textract 中转移出去,他们可以直接使用与特定缺陷(旋转、噪声、低对比度或分辨率不足)相匹配的过滤器来解决根本原因。 预处理功能页面和图像质量校正指南记录了可用的滤镜及其适用场景。
常见问题解答
我为什么要从 Amazon Textract 迁移到 IronOCR?
常见的驱动因素包括消除 COM 互操作的复杂性、取代基于文件的许可证管理、避免按页计费、启用 Docker/容器部署以及采用与标准 .NET 工具集成的 NuGet 原生工作流程。
从 Amazon Textract 迁移到 IronOCR 时,主要的代码变更有哪些?
将 Amazon Textract 初始化序列替换为 IronTesseract 实例化,移除 COM 生命周期管理(显式的创建/加载/关闭模式),并更新结果属性名称。这样可以显著减少样板代码行数。
我该如何安装 IronOCR 以开始迁移?
在软件包管理器控制台中运行“Install-Package IronOcr”,或在命令行界面中运行“dotnet add package IronOcr”。语言包是单独的软件包:例如,法语语言包需要运行“dotnet add package IronOcr.Languages.French”。
IronOCR 对标准商业文档的 OCR 准确率是否能与 Amazon Textract 相媲美?
IronOCR 对标准商业内容(包括发票、合同、收据和打印表格)的识别准确率很高。图像预处理滤镜(例如去倾斜、去噪、对比度增强)可进一步提高对质量较差输入文件的识别率。
IronOCR 如何处理 Amazon Textract 单独安装的语言数据?
IronOCR 中的语言数据以 NuGet 包的形式分发。“dotnet add package IronOcr.Languages.German”命令用于安装德语支持。无需手动放置文件或指定目录路径。
从 Amazon Textract 迁移到 IronOCR 是否需要更改部署基础设施?
IronOCR 所需的架构变更比 Amazon Textract 少。它无需 SDK 二进制文件路径、许可证文件放置位置或许可证服务器配置。NuGet 包包含完整的 OCR 引擎,许可证密钥是应用程序代码中设置的一个字符串。
迁移后如何配置 IronOCR 许可?
在应用程序启动代码中,将 IronOcr.License.LicenseKey 赋值为 "YOUR-KEY"。在 Docker 或 Kubernetes 中,将此密钥存储为环境变量,并在启动时读取。使用 License.IsValidLicense 在接受流量之前进行验证。
IronOCR 能否像 Amazon Textract 那样处理 PDF 文件?
是的。IronOCR 可以读取原生 PDF 和扫描版 PDF。实例化 IronTesseract,调用 ocr.Read(input) 方法(其中 input 可以是 PDF 路径或 OcrPdfInput),然后遍历 OcrResult 页面。无需单独的 PDF 渲染流程。
IronOCR 如何处理大批量处理中的线程问题?
IronTesseract 可以安全地按线程实例化。在 Parallel.ForEach 或 Task 池中为每个线程启动一个实例,并发运行 OCR,并在完成后释放每个实例。无需全局状态或锁定。
IronOCR在提取文本后支持哪些输出格式?
IronOCR 返回结构化结果,包括文本、词坐标、置信度评分和页面结构。导出选项包括纯文本、可搜索 PDF 和用于下游处理的结构化结果对象。
对于可扩展的工作负载而言,IronOCR 的定价是否比 Amazon Textract 更可预测?
IronOCR采用永久统一费率许可模式,不收取任何页数或数量费用。无论您处理1万页还是1000万页,许可费用都保持不变。批量许可和团队许可选项请参见IronOCR定价页面。
从 Amazon Textract 迁移到 IronOCR 后,我现有的测试会发生什么变化?
迁移后,对提取的文本内容进行断言的测试应该继续通过。验证 API 调用模式或 COM 对象生命周期的测试需要更新,以反映 IronOCR 更简化的初始化和结果模型。

