フッターコンテンツにスキップ
ビデオ

AWS TextractからIronOCRへの移行

このガイドでは、クラウドへの依存なしにローカルでドキュメント処理を行う必要がある.NET開発者向けに、AWS TextractからIronOCRへの完全な移行手順を解説します。 このドキュメントでは、認証情報の削除、非同期パイプラインの削除、S3の削除、および各Textract操作をIronOCRの同等の操作に移行するために必要な具体的なコード置換について説明します。

AWS Textractから移行する理由

AWS Textractは優れたマネージドサービスですが、そのアーキテクチャ上、ドキュメントのワークロードが増加するにつれて、金銭的コストと運用コストの両方が増大します。 本格的な文書処理パイプラインを構築するチームは、最終的にこれらの問題点すべてに直面することになる。

AWS認証インフラストラクチャは、あらゆるデプロイメントを複雑化させます。Textractコードを実行するすべての環境には、AWS認証情報が必要です。IAMユーザーまたはロール、アクセスキーとシークレット、リージョン構成、そしてPDF処理の場合はS3バケットのアクセス許可などです。最初のページが処理されるまでに、5つの異なる構成手順が必要になります。 本番デプロイには資格情報の回転ポリシー、DockerコンテナまたはKubernetesポッドへの安全な注入、およびIAMポリシーのドリフトが静かに失敗を引き起こす際のAccessDeniedExceptionの監視が必要です。 IronOCRには、起動時に一度だけ設定するライセンスキーという文字列が1つ必要です。

ページごとの価格に上限はありません。 AnalyzeDocumentをトリガーします。これは基本料金の10倍です。 フォームは1ページにつき0.05ドル追加料金がかかります。 表抽出機能を含む、月間10万ページの混合文書ワークフローの費用は年間1万8000ドルで、この金額は毎年1月にリセットされます。 IronOCRProfessionalライセンスの価格は、2,999ドルの一括払いとなります。 その後は、ページ数に関わらず、1ページあたりの料金はゼロになります。

非同期PDFパイプラインはメンテナンスの責任です。 Textractでの多ページPDFの処理には、S3へのアップロード、StartDocumentTextDetection、ポーリングループ、ページネートされた結果の取得、S3のクリーンアップの5つの異なるフェーズが必要です。各フェーズは独立した失敗モードであり、それぞれにエラーハンドリング、リトライ戦略、タイムアウト管理が必要です。 このパイプラインを本番環境に導入したチームは、構築に費やした時間よりも、保守に費やした時間の方がはるかに長いと一貫して報告している。 IronOCRは、2行のコードでPDFを読み取ります。

インターネットにアクセスできないと、Textract も利用できません。隔離されたネットワークセグメント内の Docker コンテナ、オンプレミスサーバー、エアギャップされた研究環境、およびアウトバウンドトラフィックが制限されている産業システムはすべて、Textract が利用できないという共通の特徴を持っています。 IronOCRはNuGetパッケージとしてインストールされ、最初のパッケージダウンロード後はネットワーク呼び出しなしで動作します。

データは、呼び出しのたびにお客様のインフラストラクチャから送信されます。Textractを使用したOCR操作はすべて、ドキュメントの内容をAmazonのサーバーに送信します。 PHIを処理する医療機関、CUIを扱う防衛関連企業、機密通信を処理する法務チーム、決済カード画像を処理する金融機関にとって、これは設定オプションではなく、規制環境におけるTextractの使用を完全に禁止するアーキテクチャ上の制約です。 IronOCRは、お使いのハードウェア上で処理を実行します。 クラウドモードを無効にする機能はありません。 ローカル処理のみが唯一のモードです。

レート制限はバッチスループットを制約します。 デフォルトのStartDocumentTextDetection TPSリミットは1秒あたり5リクエストです。 数百のドキュメントを処理するバッチジョブには、ProvisionedThroughputExceededExceptionの指数的バックオフ、およびレート補充タイマーが必要です。 TPS(1秒あたりのトランザクション数)の増加をリクエストするには、正式なAWSサポートケースを申請する必要があり、必ずしも結果が保証されるわけではありません。 IronOCRはローカルCPUが許す速さで処理します–16コアのサーバーはParallel.ForEachを使用して16のドキュメントを同時に処理し、サービス階層の交渉は必要ありません。

基本的な問題

Textractの導入はすべて、1ページも処理される前に、この儀式から始まります。

// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call

// Environment must have:
//   AWS_ACCESS_KEY_ID=AKIA...
//   AWS_SECRET_ACCESS_KEY=...
//   AWS_DEFAULT_REGION=us-east-1
//   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
//   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
//   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

public class TextractOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client; // required for PDFs

    public TextractOcrService()
    {
        // Fails with AmazonClientException if credentials not found in chain
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
    }
}
// Textract: five environment variables, an IAM role, and an S3 bucket
// — all required before the first OCR call

// Environment must have:
//   AWS_ACCESS_KEY_ID=AKIA...
//   AWS_SECRET_ACCESS_KEY=...
//   AWS_DEFAULT_REGION=us-east-1
//   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
//   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
//   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

public class TextractOcrService
{
    private readonly AmazonTextractClient _textractClient;
    private readonly AmazonS3Client _s3Client; // required for PDFs

    public TextractOcrService()
    {
        // Fails with AmazonClientException if credentials not found in chain
        _textractClient = new AmazonTextractClient(Amazon.RegionEndpoint.USEast1);
        _s3Client = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
    }
}
Imports Amazon
Imports Amazon.Textract
Imports Amazon.S3

' Textract: five environment variables, an IAM role, and an S3 bucket
' — all required before the first OCR call

' Environment must have:
'   AWS_ACCESS_KEY_ID=AKIA...
'   AWS_SECRET_ACCESS_KEY=...
'   AWS_DEFAULT_REGION=us-east-1
'   IAM role: textract:DetectDocumentText, textract:AnalyzeDocument
'   IAM role: s3:PutObject, s3:DeleteObject (for any PDF processing)
'   S3 bucket: my-textract-staging-bucket (with lifecycle policy to prevent cost accumulation)

Public Class TextractOcrService
    Private ReadOnly _textractClient As AmazonTextractClient
    Private ReadOnly _s3Client As AmazonS3Client ' required for PDFs

    Public Sub New()
        ' Fails with AmazonClientException if credentials not found in chain
        _textractClient = New AmazonTextractClient(Amazon.RegionEndpoint.USEast1)
        _s3Client = New AmazonS3Client(Amazon.RegionEndpoint.USEast1)
    End Sub
End Class
$vbLabelText   $csharpLabel

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

IronOCRとAWS Textract:機能比較

以下の表は、移行の意思決定において最も重要な側面に関して、両ライブラリの機能をマッピングしたものです。

フィーチャー AWS テクストラクト IronOCR
加工場所 Amazonクラウド(必須) 地域限定/店内飲食のみ
インターネットが必要です。 いつも 一度もない
認証情報の設定 IAMユーザー/ロール + オプションのS3バケット 単一ライセンスキー文字列
複数ページPDF S3ステージングと非同期ジョブが必要です 同期input.LoadPdf()の直接
パスワードで保護されたPDF サポートされていません input.LoadPdf(path, Password: "x")
マルチフレームTIFF サポートされていません input.LoadImageFrames("multi.tiff")
非同期ポーリングが必要です はい(すべてのPDFファイルに対応) いいえ — デフォルトでは同期
料金の制限 5 TPS デフォルト (StartDocumentTextDetection) なし — CPUバウンドのみ
ページあたりの料金 1ページあたり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
SHELL

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

ステップ 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"
$vbLabelText   $csharpLabel

コード移行の例

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

IronOCRのアプローチ:

// Requires: NuGet IronOcr
// Requires: one license key string — nothing else

using IronOcr;

public class DocumentOcrService
{
    private readonly IronTesseract _ocr;

    public DocumentOcrService()
    {
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
        _ocr = new IronTesseract();
    }

    public string ReadImage(string imagePath)
    {
        // No credential chain, no region, no bucket — just read
        return _ocr.Read(imagePath).Text;
    }
}
// Requires: NuGet IronOcr
// Requires: one license key string — nothing else

using IronOcr;

public class DocumentOcrService
{
    private readonly IronTesseract _ocr;

    public DocumentOcrService()
    {
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY"; // or set at Program.cs startup
        _ocr = new IronTesseract();
    }

    public string ReadImage(string imagePath)
    {
        // No credential chain, no region, no bucket — just read
        return _ocr.Read(imagePath).Text;
    }
}
Imports IronOcr

Public Class DocumentOcrService
    Private ReadOnly _ocr As IronTesseract

    Public Sub New()
        IronOcr.License.LicenseKey = "YOUR-LICENSE-KEY" ' or set at Program.vb startup
        _ocr = New IronTesseract()
    End Sub

    Public Function ReadImage(imagePath As String) As String
        ' No credential chain, no region, no bucket — just read
        Return _ocr.Read(imagePath).Text
    End Function
End Class
$vbLabelText   $csharpLabel

IAMロール、環境変数、~/.aws/credentialsファイル、S3バケットライフサイクルポリシー、リージョン設定など、AWSの資格情報インフラ全体が削除されます。 DocumentOcrServiceコンストラクタは、ランタイムの失敗モードを持つ3つの依存関係挿入サイトから、単一のライセンス割り当てに変わります。 デプロイメントパターンの詳細については、 IronTesseract セットアップガイドを参照してください。

StartDocumentTextDetectionをTIFFマルチフレーム処理に置き換える

Textractの非同期ジョブAPI (StartDocumentTextDetection)は、多ページドキュメントに必須です。 文書のデジタル化パイプラインでよく見られるパターンは、スキャンされた文書をマルチフレームTIFFファイル(スキャンされたページごとに1フレーム)として受け取ることです。 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
$vbLabelText   $csharpLabel

IronOCRのアプローチ:

// IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling

using IronOcr;

public string ProcessScannedTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath); // reads all frames natively

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    // Access per-page results if needed
    foreach (var page in result.Pages)
        Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");

    return result.Text;
}
// IronOCR reads multi-frame TIFF directly — no conversion, no S3, no polling

using IronOcr;

public string ProcessScannedTiff(string tiffPath)
{
    using var input = new OcrInput();
    input.LoadImageFrames(tiffPath); // reads all frames natively

    var ocr = new IronTesseract();
    var result = ocr.Read(input);

    // Access per-page results if needed
    foreach (var page in result.Pages)
        Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected");

    return result.Text;
}
Imports IronOcr

Public Function ProcessScannedTiff(tiffPath As String) As String
    Using input As New OcrInput()
        input.LoadImageFrames(tiffPath) ' reads all frames natively

        Dim ocr As New IronTesseract()
        Dim result = ocr.Read(input)

        ' Access per-page results if needed
        For Each page In result.Pages
            Console.WriteLine($"Page {page.PageNumber}: {page.Words.Length} words detected")
        Next

        Return result.Text
    End Using
End Function
$vbLabelText   $csharpLabel

TIFFからPDFへの変換ステップ、S3へのアップロード、非同期ポーリングループ、ページ分割された結果の蓄積、およびS3のクリーンアップはすべて省略されます。 IronOCRの実装はフレームを直接読み取り、result.Pagesを通じてページごとのアクセスを提供します。中間フォーマット変換は行いません。 TIFFおよびGIF入力ガイドでは、フレームの一部のみが必要な場合のフレーム選択について説明しています。

S3ステージングを検索可能なPDF出力に置き換える

Textractの一般的な使用例としては、スキャンしたPDFアーカイブを検索可能な形式に変換することが挙げられます。 Textractの手法では、各PDFをS3にアップロードし、非同期ジョブを実行して抽出されたテキストを収集し、その後、別のPDFライブラリを使用してそのテキストを検索可能なレイヤーとして埋め込む必要があります。 IronOCRは、1回の呼び出しでスキャンを実行し、検索可能な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
$vbLabelText   $csharpLabel

IronOCRのアプローチ:

// IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed

using IronOcr;

public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
    var ocr = new IronTesseract();

    using var input = new OcrInput();
    input.LoadPdf(scannedPdfPath);

    var result = ocr.Read(input);

    // Embed extracted text as searchable layer in one call
    result.SaveAsSearchablePdf(outputPath);

    Console.WriteLine($"Pages processed: {result.Pages.Length}");
    Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
// IronOCR reads the PDF and produces a searchable PDF output directly
// No S3, no polling, no external PDF library needed

using IronOcr;

public void ConvertToSearchablePdf(string scannedPdfPath, string outputPath)
{
    var ocr = new IronTesseract();

    using var input = new OcrInput();
    input.LoadPdf(scannedPdfPath);

    var result = ocr.Read(input);

    // Embed extracted text as searchable layer in one call
    result.SaveAsSearchablePdf(outputPath);

    Console.WriteLine($"Pages processed: {result.Pages.Length}");
    Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
}
Imports IronOcr

Public Sub ConvertToSearchablePdf(scannedPdfPath As String, outputPath As String)
    Dim ocr As New IronTesseract()

    Using input As New OcrInput()
        input.LoadPdf(scannedPdfPath)

        Dim result = ocr.Read(input)

        ' Embed extracted text as searchable layer in one call
        result.SaveAsSearchablePdf(outputPath)

        Console.WriteLine($"Pages processed: {result.Pages.Length}")
        Console.WriteLine($"Overall confidence: {result.Confidence:F1}%")
    End Using
End Sub
$vbLabelText   $csharpLabel

ポーリングのタイムアウトロジック、S3へのアップロードとクリーンアップのパターン、および外部PDFライブラリへの依存関係はすべてなくなりました。 SaveAsSearchablePdfは認識されたテキストを出力ファイルに直接埋め込み、スキャンされたすべてのページが2番目のライブラリなしで全文検索可能になります。 検索可能な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
$vbLabelText   $csharpLabel

IronOCRのアプローチ:

// IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging

using IronOcr;

public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
    var ocr = new IronTesseract();
    var result = ocr.Read(imagePath);

    var sections = new List<DocumentSection>();

    foreach (var page in result.Pages)
    {
        foreach (var paragraph in page.Paragraphs)
        {
            sections.Add(new DocumentSection
            {
                Text = paragraph.Text,
                LineCount = paragraph.Lines.Length,
                // Coordinate access: exact pixel position on the page
                LocationX = paragraph.X,
                LocationY = paragraph.Y,
                Width = paragraph.Width,
                Height = paragraph.Height,
                Confidence = paragraph.Confidence
            });
        }
    }

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public int LocationX { get; set; }
    public int LocationY { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public double Confidence { get; set; }
}
// IronOCR returns a typed hierarchy — paragraphs are first-class objects
// No block graph, no heuristic gap detection, no confidence averaging

using IronOcr;

public List<DocumentSection> ExtractDocumentStructure(string imagePath)
{
    var ocr = new IronTesseract();
    var result = ocr.Read(imagePath);

    var sections = new List<DocumentSection>();

    foreach (var page in result.Pages)
    {
        foreach (var paragraph in page.Paragraphs)
        {
            sections.Add(new DocumentSection
            {
                Text = paragraph.Text,
                LineCount = paragraph.Lines.Length,
                // Coordinate access: exact pixel position on the page
                LocationX = paragraph.X,
                LocationY = paragraph.Y,
                Width = paragraph.Width,
                Height = paragraph.Height,
                Confidence = paragraph.Confidence
            });
        }
    }

    return sections;
}

public class DocumentSection
{
    public string Text { get; set; }
    public int LineCount { get; set; }
    public int LocationX { get; set; }
    public int LocationY { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
    public double Confidence { get; set; }
}
Imports IronOcr

Public Function ExtractDocumentStructure(imagePath As String) As List(Of DocumentSection)
    Dim ocr As New IronTesseract()
    Dim result = ocr.Read(imagePath)

    Dim sections As New List(Of DocumentSection)()

    For Each page In result.Pages
        For Each paragraph In page.Paragraphs
            sections.Add(New DocumentSection With {
                .Text = paragraph.Text,
                .LineCount = paragraph.Lines.Length,
                .LocationX = paragraph.X,
                .LocationY = paragraph.Y,
                .Width = paragraph.Width,
                .Height = paragraph.Height,
                .Confidence = paragraph.Confidence
            })
        Next
    Next

    Return sections
End Function

Public Class DocumentSection
    Public Property Text As String
    Public Property LineCount As Integer
    Public Property LocationX As Integer
    Public Property LocationY As Integer
    Public Property Width As Integer
    Public Property Height As Integer
    Public Property Confidence As Double
End Class
$vbLabelText   $csharpLabel

段落、行、単語は型付きコレクションとして.Confidenceプロパティで直接アクセス可能です。 バウンディングボックスの正規化、ギャップ閾値のヒューリスティック、ブロックタイプのフィルタリングは行いません。 読み取り結果ガイドでは、文字レベルの座標アクセスを含む完全なOcrResult階層が文書化されています。 この構造に依存する請求書および領収書のワークフローについては、請求書OCRチュートリアルを参照してください。

処理速度制限のあるバッチ処理を、制約のない並列実行に置き換える

バッチ画像処理では、TextractのTPS制限が最も顕著に運用コストに影響を与えます。DetectDocumentText同期APIにはレート制限があります; 1秒あたり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
$vbLabelText   $csharpLabel

IronOCRのアプローチ:

// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores

using IronOcr;
using System.Collections.Concurrent;

public Dictionary<string, string> ProcessImageBatch(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var ocr = new IronTesseract();
    var results = new ConcurrentDictionary<string, string>();
    int completed = 0;

    Parallel.ForEach(imagePaths, path =>
    {
        var result = ocr.Read(path);
        results[path] = result.Text;

        var done = Interlocked.Increment(ref completed);
        progress?.Report((done, imagePaths.Count));
    });

    return results.ToDictionary(x => x.Key, x => x.Value);
}
// No rate limits, no semaphore, no retry logic — Parallel.ForEach saturates all cores

using IronOcr;
using System.Collections.Concurrent;

public Dictionary<string, string> ProcessImageBatch(
    IReadOnlyList<string> imagePaths,
    IProgress<(int Completed, int Total)> progress = null)
{
    var ocr = new IronTesseract();
    var results = new ConcurrentDictionary<string, string>();
    int completed = 0;

    Parallel.ForEach(imagePaths, path =>
    {
        var result = ocr.Read(path);
        results[path] = result.Text;

        var done = Interlocked.Increment(ref completed);
        progress?.Report((done, imagePaths.Count));
    });

    return results.ToDictionary(x => x.Key, x => x.Value);
}
Imports IronOcr
Imports System.Collections.Concurrent
Imports System.Threading

Public Function ProcessImageBatch(
    imagePaths As IReadOnlyList(Of String),
    Optional progress As IProgress(Of (Completed As Integer, Total As Integer)) = Nothing) As Dictionary(Of String, String)

    Dim ocr As New IronTesseract()
    Dim results As New ConcurrentDictionary(Of String, String)()
    Dim completed As Integer = 0

    Parallel.ForEach(imagePaths, Sub(path)
                                     Dim result = ocr.Read(path)
                                     results(path) = result.Text

                                     Dim done = Interlocked.Increment(completed)
                                     If progress IsNot Nothing Then
                                         progress.Report((done, imagePaths.Count))
                                     End If
                                 End Sub)

    Return results.ToDictionary(Function(x) x.Key, Function(x) x.Value)
End Function
$vbLabelText   $csharpLabel

ProvisionedThroughputExceededExceptionキャッチブロックはすべて削除されます。 IronTesseractはスレッドセーフで、スレッド間で共有される単一インスタンスがロックなしで完全な並列負荷を処理します。 8コアのマシンでは、設定不要で8つのドキュメントを同時に処理できます。 32コアで、32コアを同時に処理します。 マルチスレッドの例では完全なパターンを示しており、速度最適化ガイドではスループット重視のデプロイメントにおけるエンジン構成のチューニングについて解説しています。

AnalyzeDocument Form Extraction を Region-Based CropRectangle OCR に置き換える

TextractのRelationshipType.VALUEの関係でリンクして返します。 フォームフィールドのキーと値のペアを再構築するには、WORDブロックを取得してテキストを組み立てる必要があります。 フィールド位置が固定されている固定形式フォームの場合、IronOCRのCropRectangleは関係グラフのトラバースをせずに各フィールドを直接抽出し、1ページ当たりのコストはゼロです。

AWS Textract のアプローチ:

// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    // $0.05/page for FORMS analysis
    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "FORMS" }
        });

    var allBlocks = response.Blocks;
    var fields = new Dictionary<string, string>();

    // Find KEY blocks only
    var keyBlocks = allBlocks.Where(b =>
        b.BlockType == BlockType.KEY_VALUE_SET &&
        b.EntityTypes?.Contains("KEY") == true);

    foreach (var keyBlock in keyBlocks)
    {
        // Assemble key text from CHILD WORD blocks
        var keyText = GetTextFromBlock(allBlocks, keyBlock);

        // Find associated VALUE block via VALUE relationship
        var valueBlockId = keyBlock.Relationships?
            .FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
            .Ids.FirstOrDefault();

        if (valueBlockId == null) continue;

        var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
        if (valueBlock == null) continue;

        var valueText = GetTextFromBlock(allBlocks, valueBlock);
        fields[keyText.TrimEnd(':')] = valueText;
    }

    return fields;
}

private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
    if (block.Relationships == null) return string.Empty;

    var childWordIds = block.Relationships
        .Where(r => r.Type == RelationshipType.CHILD)
        .SelectMany(r => r.Ids);

    return string.Join(" ", allBlocks
        .Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
        .Select(b => b.Text));
}
// FORMS mode costs $0.05/page — the most expensive Textract tier
// Relationship graph traversal required to reconstruct key-value pairs

using Amazon.Textract;
using Amazon.Textract.Model;

public async Task<Dictionary<string, string>> ExtractInvoiceFieldsAsync(string imagePath)
{
    var bytes = File.ReadAllBytes(imagePath);

    // $0.05/page for FORMS analysis
    var response = await _textractClient.AnalyzeDocumentAsync(
        new AnalyzeDocumentRequest
        {
            Document = new Document { Bytes = new MemoryStream(bytes) },
            FeatureTypes = new List<string> { "FORMS" }
        });

    var allBlocks = response.Blocks;
    var fields = new Dictionary<string, string>();

    // Find KEY blocks only
    var keyBlocks = allBlocks.Where(b =>
        b.BlockType == BlockType.KEY_VALUE_SET &&
        b.EntityTypes?.Contains("KEY") == true);

    foreach (var keyBlock in keyBlocks)
    {
        // Assemble key text from CHILD WORD blocks
        var keyText = GetTextFromBlock(allBlocks, keyBlock);

        // Find associated VALUE block via VALUE relationship
        var valueBlockId = keyBlock.Relationships?
            .FirstOrDefault(r => r.Type == RelationshipType.VALUE)?
            .Ids.FirstOrDefault();

        if (valueBlockId == null) continue;

        var valueBlock = allBlocks.FirstOrDefault(b => b.Id == valueBlockId);
        if (valueBlock == null) continue;

        var valueText = GetTextFromBlock(allBlocks, valueBlock);
        fields[keyText.TrimEnd(':')] = valueText;
    }

    return fields;
}

private string GetTextFromBlock(IList<Block> allBlocks, Block block)
{
    if (block.Relationships == null) return string.Empty;

    var childWordIds = block.Relationships
        .Where(r => r.Type == RelationshipType.CHILD)
        .SelectMany(r => r.Ids);

    return string.Join(" ", allBlocks
        .Where(b => b.BlockType == BlockType.WORD && childWordIds.Contains(b.Id))
        .Select(b => b.Text));
}
Imports Amazon.Textract
Imports Amazon.Textract.Model
Imports System.IO
Imports System.Threading.Tasks

Public Class InvoiceFieldExtractor
    Private _textractClient As AmazonTextractClient

    Public Async Function ExtractInvoiceFieldsAsync(imagePath As String) As Task(Of Dictionary(Of String, String))
        Dim bytes = File.ReadAllBytes(imagePath)

        ' $0.05/page for FORMS analysis
        Dim response = Await _textractClient.AnalyzeDocumentAsync(
            New AnalyzeDocumentRequest With {
                .Document = New Document With {.Bytes = New MemoryStream(bytes)},
                .FeatureTypes = New List(Of String) From {"FORMS"}
            })

        Dim allBlocks = response.Blocks
        Dim fields As New Dictionary(Of String, String)()

        ' Find KEY blocks only
        Dim keyBlocks = allBlocks.Where(Function(b) 
                                            Return b.BlockType = BlockType.KEY_VALUE_SET AndAlso
                                                   b.EntityTypes?.Contains("KEY") = True
                                        End Function)

        For Each keyBlock In keyBlocks
            ' Assemble key text from CHILD WORD blocks
            Dim keyText = GetTextFromBlock(allBlocks, keyBlock)

            ' Find associated VALUE block via VALUE relationship
            Dim valueBlockId = keyBlock.Relationships?.
                FirstOrDefault(Function(r) r.Type = RelationshipType.VALUE)?.
                Ids.FirstOrDefault()

            If valueBlockId Is Nothing Then Continue For

            Dim valueBlock = allBlocks.FirstOrDefault(Function(b) b.Id = valueBlockId)
            If valueBlock Is Nothing Then Continue For

            Dim valueText = GetTextFromBlock(allBlocks, valueBlock)
            fields(keyText.TrimEnd(":"c)) = valueText
        Next

        Return fields
    End Function

    Private Function GetTextFromBlock(allBlocks As IList(Of Block), block As Block) As String
        If block.Relationships Is Nothing Then Return String.Empty

        Dim childWordIds = block.Relationships.
            Where(Function(r) r.Type = RelationshipType.CHILD).
            SelectMany(Function(r) r.Ids)

        Return String.Join(" ", allBlocks.
            Where(Function(b) b.BlockType = BlockType.WORD AndAlso childWordIds.Contains(b.Id)).
            Select(Function(b) b.Text))
    End Function
End Class
$vbLabelText   $csharpLabel

IronOCRのアプローチ:

// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known

using IronOcr;

// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
    new Dictionary<string, CropRectangle>
    {
        { "InvoiceNumber", new CropRectangle(450, 80,  300, 35) },
        { "InvoiceDate",   new CropRectangle(450, 120, 300, 35) },
        { "VendorName",    new CropRectangle(50,  160, 400, 35) },
        { "TotalAmount",   new CropRectangle(450, 680, 200, 35) },
        { "DueDate",       new CropRectangle(450, 720, 200, 35) }
    };

public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
    var ocr = new IronTesseract();
    var fields = new Dictionary<string, string>();

    foreach (var zone in InvoiceFieldZones)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, zone.Value); // load only the defined region

        var result = ocr.Read(input);
        fields[zone.Key] = result.Text.Trim();
    }

    return fields;
}
// CropRectangle extracts each field zone directly — no relationship graph
// Works for any fixed-format form where field positions are known

using IronOcr;

// Define the field zones for your form template once
private static readonly Dictionary<string, CropRectangle> InvoiceFieldZones =
    new Dictionary<string, CropRectangle>
    {
        { "InvoiceNumber", new CropRectangle(450, 80,  300, 35) },
        { "InvoiceDate",   new CropRectangle(450, 120, 300, 35) },
        { "VendorName",    new CropRectangle(50,  160, 400, 35) },
        { "TotalAmount",   new CropRectangle(450, 680, 200, 35) },
        { "DueDate",       new CropRectangle(450, 720, 200, 35) }
    };

public Dictionary<string, string> ExtractInvoiceFields(string imagePath)
{
    var ocr = new IronTesseract();
    var fields = new Dictionary<string, string>();

    foreach (var zone in InvoiceFieldZones)
    {
        using var input = new OcrInput();
        input.LoadImage(imagePath, zone.Value); // load only the defined region

        var result = ocr.Read(input);
        fields[zone.Key] = result.Text.Trim();
    }

    return fields;
}
Imports IronOcr

' CropRectangle extracts each field zone directly — no relationship graph
' Works for any fixed-format form where field positions are known

' Define the field zones for your form template once
Private Shared ReadOnly InvoiceFieldZones As New Dictionary(Of String, CropRectangle) From {
    {"InvoiceNumber", New CropRectangle(450, 80, 300, 35)},
    {"InvoiceDate", New CropRectangle(450, 120, 300, 35)},
    {"VendorName", New CropRectangle(50, 160, 400, 35)},
    {"TotalAmount", New CropRectangle(450, 680, 200, 35)},
    {"DueDate", New CropRectangle(450, 720, 200, 35)}
}

Public Function ExtractInvoiceFields(imagePath As String) As Dictionary(Of String, String)
    Dim ocr As New IronTesseract()
    Dim fields As New Dictionary(Of String, String)()

    For Each zone In InvoiceFieldZones
        Using input As New OcrInput()
            input.LoadImage(imagePath, zone.Value) ' load only the defined region

            Dim result = ocr.Read(input)
            fields(zone.Key) = result.Text.Trim()
        End Using
    Next

    Return fields
End Function
$vbLabelText   $csharpLabel

GetTextFromBlockヘルパーは排除されます。 各フィールドは、それを含むピクセル領域を正確に読み取ります。それ以上でもそれ以下でもありません。 領域ベースのOCRガイドは、CropRectangleの座標とテンプレート測定からのゾーンの定義方法を説明しています。 請求書固有のワークフローについては、請求書OCRに関するブログ記事領収書スキャンチュートリアルで、フィールド抽出パイプライン全体を解説しています。

AWS テクストラクト API からIronOCRへのマッピング リファレンス

AWS テクストラクト IronOCR相当値
AmazonTextractClient IronTesseract
AmazonS3Client 不要
DetectDocumentTextRequest input.LoadImage()
DetectDocumentTextResponse OcrResult
AnalyzeDocumentRequest (TABLES/FORMS) CropRectangle
StartDocumentTextDetectionRequest input.LoadPdf() — 同期、ジョブ開始なし
GetDocumentTextDetectionRequest 該当なし — 結果は即座に判明します
Document.Bytes input.LoadImage(stream)
S3Object (ドキュメントステージング) ファイルパスまたはストリーム - ステージングは​​不要
Block (BlockType.LINE) result.Lines (型付きコレクション)
Block (BlockType.WORD) result.Words (型付きコレクション)
Block (BlockType.TABLE) result.Wordsが座標の近接によってグループ化されます
Block (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のような機能はありません)

一般的な移行の問題と解決策

問題1:新規展開環境における認証情報の設定ミス

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

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

既に非UIスレッドで動作しているバッチプロセッサやバックグラウンドサービスの場合、Task.Runはそのコンテキストで利益なしにオーバーヘッドを追加します。 非同期OCRガイドでは、推奨されるパターンについて説明しています。

問題3:S3バケットのクリーンアップロジックがコード全体に散在している

AWS Textract: Textract処理のためにS3にアップロードされるコードは、処理完了後に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
$vbLabelText   $csharpLabel

移行後、S3ステージングバケットを廃止してください。 PDF入力ガイドストリーム入力ガイドは、 S3ステージングされたドキュメントアクセスを置き換えるすべての入力パターンを網羅しています。

問題4:ブロックグラフ走査コードには直接的な同等物がない

AWS Textract: RelationshipType.CHILD ID配列からテーブルセル、フォームフィールド、段落グループを再構築するコードは、移行に最も時間がかかります。 IronOCRは関係グラフではなく型付きコレクションを返すため、1対1の置換は不可能です。

解決策:各トラバーサルパターンを適切な型付きプロパティに置き換えます。 リレーションシップ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
$vbLabelText   $csharpLabel

result.Wordsの単語座標グループ化を使用します。 表の読み方ガイドでは、座標に基づくアプローチについて説明しています。

問題5:ProvisionedThroughputExceededExceptionキャッチブロック

AWS Textract: ロバストなバッチ処理コードは、ErrorCode == "ProvisionedThroughputExceededException"でキャッチし、バックオフ付きのリトライを実装します。 このエラーコードはTextract固有の概念であり、 IronOCRには同等のコードはありません。

解決策: すべてのProvisionedThroughputExceededExceptionキャッチブロックを削除します。 IronOCRにはTPS(1秒あたりの処理速度)の制限はありません。 完全なスロットルバッチパターンをParallel.ForEachに置き換えます。

// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
    var result = new IronTesseract().Read(path);
    results[path] = result.Text;
});
// Delete: SemaphoreSlim, retry loops, ProvisionedThroughputExceededException handlers
// Replace with:
Parallel.ForEach(imagePaths, path =>
{
    var result = new IronTesseract().Read(path);
    results[path] = result.Text;
});
Imports System.Threading.Tasks

Parallel.ForEach(imagePaths, Sub(path)
    Dim result = New IronTesseract().Read(path)
    results(path) = result.Text
End Sub)
$vbLabelText   $csharpLabel

問題6:クライアントコンストラクタに組み込まれたリージョン構成

AWS Textract: new AmazonTextractClient(Amazon.RegionEndpoint.USEast1)はリージョンをハードコーディングします。 複数リージョン展開には、複数のクライアントインスタンスまたは動的なリージョン選択が必要です。 Textractが新しい地域でのサポートを追加する場合、コードを更新する必要があります。

解決策: IronOCRには領域の概念がありません。 すべてのAmazon.RegionEndpointリファレンスを削除します。 IronTesseractコンストラクタはネットワーク構成を受け取らず、処理はローカルです。 AWSインフラストラクチャ上で各リージョンが独自のコンピューティングを実行するマルチリージョン展開の場合、各IronOCRインスタンスは、リージョン間呼び出しを行うことなく、そのコンピューティング境界内でドキュメントをローカルに処理します。

AWS テクストラクト 移行チェックリスト

移行前

コードベース内の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" .
SHELL

代替コードを作成する前に、以下の項目を確認してください。

  • AmazonTextractClientを含むすべてのファイルをカウントします—各ファイルが置換対象です
  • すべてのFORMSの使用があるかどうかを確認します
  • すべての非同期ポーリングループを特定します (do { await Task.Delay } while JobStatus == IN_PROGRESS)
  • すべてのS3クリーンアップfinallyブロックを見つけます—これらはまるごと削除され、置換はしません
  • すべてのProvisionedThroughputExceededExceptionキャッチブロックをカウントします—削除し、置換はしません
  • すべてのBlockType.KEY_VALUE_SETトラバースロジックを示しておきます—それぞれに構造化された出力置換が必要です

コードの移行

  1. すべてのAWSSDK.Coreを削除します
  2. OCRを実行する各プロジェクトでdotnet add package IronOcrを実行します
  3. アプリケーション起動時にIronOcr.License.LicenseKey = ...を1度追加します (Program.cs または同等のもの)
  4. すべてのusing Amazon.Runtime;文を削除します
  5. 以前にTextract名前空間をインポートしていた各ファイルにusing IronOcr;を追加します
  6. new IronTesseract()に置き換えます
  7. PutObjectAsync / DeleteObjectAsync呼び出しを直接ファイルパスまたはストリームリードに置き換えます
  8. すべてのDetectDocumentTextAsync呼び出しを置き換えます: var text = ocr.Read(path).Text
  9. すべてのStartDocumentTextDetectionAsync + ポーリングループをinput.LoadPdf(path) + ocr.Read(input)に置き換えます
  10. すべての多フレームTIFFパイプライン (TIFF → PDF → S3 → 非同期) をinput.LoadImageFrames(tiffPath)に置き換えます
  11. BlockType.LINE / result.Lines / result.Wordsに置き換えます
  12. CropRectangleのゾーン抽出に置き換えます
  13. すべてのSemaphoreSlimスロットルを削除します
  14. スロットルバッチパターンを共有のParallel.ForEachで置き換えます
  15. デプロイマニフェストとCIパイプラインから、すべてのAWS認証情報環境変数設定を削除します。
  16. すべての処理コードの移行と検証が完了したら、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パッケージに統合。3つの AWS SDK パッケージ、ライフサイクル ポリシーを含む S3 バケット、すべてのデプロイ環境にわたる IAM ロール、および AWS 認証情報のローテーション手順が、1 つのNuGetパッケージに置き換えられます。 dotnet add package IronOcrです。 AWS認証情報管理に伴うあらゆる下流工程(セキュリティ監査、ローテーションスケジュール、環境変数管理、Dockerシークレットの設定など)は、これと共に消滅します。

総所有コストの予測可能性。ページ単位の課金モデルでは、文書量に応じて変動するコストが際限なく増加します。 IronOCRに移行後は、処理量に関わらず、追加ページあたりのコストはゼロになります。 Textract AnalyzeDocument料金で1か月に200,000ページを処理するチームは、OCRにのみ年間$36,000を費やします。 IronOCR Enterpriseライセンス(2,999ドル)は、請求が発生しないことで31日以内にその費用を回収できます。 ライセンス階層の詳細およびSaaS再配布条件については、 IronOCRのライセンスページをご覧ください。

同期処理により、5段階の非同期処理の複雑さが解消されます。S3へのアップロード、ジョブの開始、ポーリングループ、ページ分割された結果の収集、およびクリーンアップの最終ブロックは、Textract PDFパイプラインにおける5つの独立した障害モードを表しています。移行後、PDFは2行で読み込まれます。 エラーハンドリングはocr.Read()呼び出しの周りの単一のtry/catchに減少します。 ポーリングのタイムアウトロジック、nextTokenページネーションアキュムレータ—これらの概念はIronOCRコードベースには存在しません。 メンテナンスの負担は、削除されたコードの量に比例して減少する。

完全な導入柔軟性。Textractは、OCR処理のたびにインターネット接続を必要とします。 IronOCRは、インストール時にNuGetパッケージをダウンロードするためだけにインターネットアクセスを必要とします。その後は、エアギャップネットワーク、アウトバウンドルールが設定されていないDockerコンテナ、オンプレミスサーバー、およびTextractへのアクセス権限を持たないAWS EC2インスタンスで動作します。 Windows Server上で本番環境で実行されるバイナリは、Linuxコンテナ内でも実行されます。 Docker導入ガイドLinux導入ガイドでは、これまでTextractの使用が制限されていたコンテナ環境向けの具体的な構成について説明しています。

データ主権は構成ではなくアーキテクチャによって実現されます。IronOCRにはネットワーク送信モードを無効にする機能はありません。ローカル処理のみが唯一のモードです。 IronOCRで処理された文書は、ホストマシンから外部に持ち出されることはありません。PHI(保護対象医療情報)を処理する医療アプリケーション、CUI(機密情報)を処理する防衛アプリケーション、および決済カード画像を処理する金融アプリケーションにとって、これはBAA(事業提携契約)の交渉、地域別データ所在地の設定、およびAmazonのデータ処理ポリシーの監査を必要としないコンプライアンス体制です。 コンプライアンスの範囲は、既に管理している自社のインフラストラクチャの境界です。 IronOCRの製品ページには、コンプライアンスレビューを実施するチーム向けに、機能概要と導入に関する詳細なドキュメントが掲載されています。

文書品質管理のための設定可能な前処理。Textractの内部前処理機能はアクセスも設定もできません。 スキャンした文書の仕上がりが悪い場合、唯一の対処法は出力結果を受け入れるか、再スキャンすることである。 IronOCRは完全な前処理パイプラインを公開します: DeepCleanBackgroundNoise()。 スキャン結果の信頼性が低いという理由でTextractからドキュメントを移行したチームは、回転、ノイズ、低コントラスト、解像度不足といった特定の欠陥に合わせたフィルターを使用することで、根本原因に直接対処できます。 前処理機能ページ画像品質補正ガイドには、利用可能なフィルターとその適切な使用例が記載されています。

ご注意AWS Textract, PDFSharp, Tesseract、およびiTextはそれぞれの所有者の登録商標です。 このサイトはAmazon Web Services、Google、empira Software GmbH、iText Groupとの関連、支持、またはスポンサーされておりません。すべての製品名、ロゴ、およびブランドはそれぞれの所有者の財産です。 比較は情報提供のみを目的としており、執筆時点で公開されている情報を反映しています。

よくある質問

なぜAmazon TextractからIronOCRに移行する必要があるのですか?

一般的な推進要因には、COM相互接続の複雑さの解消、ファイルベースのライセンス管理の置き換え、ページ単位の課金の回避、Docker/コンテナデプロイの有効化、標準的な.NETツールと統合するNuGetネイティブワークフローの採用などがあります。

Amazon TextractからIronOCRに移行する際の主なコード変更は何ですか?

Amazon Textractの初期化シーケンスをIronTesseractのインスタンス化に置き換え、COMのライフサイクル管理(明示的なCreate/Load/Closeパターン)を削除し、結果のプロパティ名を更新します。その結果、定型的な行が大幅に減りました。

IronOCRをインストールして移行を開始するにはどうすればいいですか?

パッケージマネージャーコンソールで'Install-Package IronOcr'を実行するか、CLIで'dotnet add package IronOcr'を実行してください。言語パックは別のパッケージです:例えばフランス語の場合は'dotnet add package IronOcr.Languages.French'となります。

IronOCRは標準的なビジネス文書のAmazon TextractのOCR精度に匹敵しますか?

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)を呼び出し、入力はPDFパスまたはOcrPdfInputであり、OcrResultページを反復します。別のPDFレンダリングパイプラインは必要ありません。

IronOCRはどのように大量処理のスレッディングを処理するのですか?

IronTesseractはスレッド毎にインスタンス化しても安全です。Parallel.ForEachまたはタスクプールでスレッドごとにインスタンスを1つスピンアップし、OCRを同時に実行し、完了したら各インスタンスを破棄します。グローバルステートやロックは必要ありません。

IronOCRはテキスト抽出後、どのような出力形式をサポートしていますか?

IronOCRはテキスト、単語座標、信頼度スコア、ページ構造を含む構造化された結果を返します。エクスポートオプションには、プレーンテキスト、検索可能なPDF、下流処理用の構造化結果オブジェクトが含まれます。

IronOCRの価格は、ワークロードのスケーリングにおいてAmazon Textractよりも予測可能ですか?

IronOCRは、定額制の永久ライセンスを採用しており、ページ単位やボリューム単位の料金はかかりません。10,000ページでも1,000万ページでも、ライセンスコストは一定です。ボリュームライセンスとチームライセンスのオプションはIronOCRの価格ページをご覧ください。

Amazon TextractからIronOCRに移行した後、既存のテストはどうなりますか?

抽出されたテキストコンテンツをアサートするテストは、移行後もパスし続ける必要があります。APIコールパターンやCOMオブジェクトのライフサイクルを検証するテストは、IronOCRのシンプルな初期化と結果モデルを反映するように更新する必要があります。

Kannaopat Udonpant
ソフトウェアエンジニア
ソフトウェアエンジニアになる前に、Kannapatは北海道大学で環境資源の博士号を修了しました。博士号を追求する間に、彼はバイオプロダクションエンジニアリング学科の一部である車両ロボティクスラボラトリーのメンバーになりました。2022年には、C#のスキルを活用してIron Softwareのエンジニアリングチームに参加し、IronPDFに注力しています。Kannapatは、IronPDFの多くのコードを執筆している開発者から直接学んでいるため、この仕事を大切にしています。同僚から学びながら、Iron Softwareでの働く社会的側面も楽しんでいます。コードやドキュメントを書いていない時は、KannapatはPS5でゲームをしたり、『The Last of Us』を再視聴したりしていることが多いです。

アイアンサポートチーム

私たちは週5日、24時間オンラインで対応しています。
チャット
メール
電話してね