IRONSOFTWAREHOME

2 GBを超えるTIFFのOCRをLibTiff.NETで

Curtis Chau
Curtis Chau
Updated: 2026年7月3日

IronOCRは、各画像を32ビット整数でインデックス化された単一のインメモリAnyBitmapバッファにロードするため、システムメモリがどれだけ利用可能かにかかわらず約2 GBで制限されます。 それ以上のサイズのTIFFはロードに失敗します。 サイズが2 GB未満になるようにBitMiracle.LibTiff.NETで大きなファイルを分割し、各チャンクをOCRします。

これはMagick.NETの回避策の完全な管理版です:デコードや再エンコードなしで、生のストリップまたはタイルレベルでページをコピーします。

ご注意: 2 GBの制限はAnyBitmapのアーキテクチャの制限であり、OSやメモリの問題ではありません。 IronOCR内部でのページごとのネイティブTIFFストリーミングはまだ利用可能ではありません。

解決策

始める前に、.NETプロジェクトにIronOCR (IronTesseract)があることを確認してください。 以下で使用するLibTiffタイプはBitMiracle.LibTiff.Classic 名前空間に存在します。

1. LibTiff.NETパッケージを追加

dotnet add package BitMiracle.LibTiff.NET
SHELL

2. TiffPageSplitterヘルパーを追加

ヘルパーは、生のストリップまたはタイルレベルで各ページをコピーするため、ピクセルデータと圧縮が正確に保持されます。 ページを1つずつストリームし、全体のファイルではなくピークメモリを1つのチャンクに抑え、サイズ制限を下回るマルチページのチャンクを生成します。

/// <summary>
/// Splits a multi-page TIFF into single-page TIFF byte streams without ever
/// holding the whole file in memory. Each page is copied at the raw
/// (still-encoded) strip/tile level, so pixel data and compression are
/// preserved exactly - there is no decode/re-encode step.
///
/// This is the chunking step only. It produces sub-2 GB single-page byte
/// arrays; feeding them to IronOCR (which is where the AnyBitmap 2 GB
/// single-buffer limit lives) is the consumer's job - see TiffOcrExample.
/// </summary>
public static class TiffPageSplitter
{
    // Tags that describe how a page's raw strip/tile data is encoded.
    // With a raw copy nothing is re-encoded, so every one of these must be
    // carried over verbatim or the copied bytes become uninterpretable.
    // Extend this list if your TIFFs carry tags not covered here
    // (e.g. ICC profiles, EXTRASAMPLES for alpha channels).
    private static readonly TiffTag[] ScalarIntTags =
    {
        TiffTag.IMAGEWIDTH,
        TiffTag.IMAGELENGTH,
        TiffTag.BITSPERSAMPLE,
        TiffTag.SAMPLESPERPIXEL,
        TiffTag.COMPRESSION,
        TiffTag.PHOTOMETRIC,
        TiffTag.FILLORDER,
        TiffTag.PLANARCONFIG,
        TiffTag.ORIENTATION,
        TiffTag.RESOLUTIONUNIT,
        TiffTag.PREDICTOR,      // required for LZW / Deflate raw copies
        TiffTag.SAMPLEFORMAT,
        TiffTag.T4OPTIONS,      // CCITT Group 3
        TiffTag.T6OPTIONS,      // CCITT Group 4
        TiffTag.SUBFILETYPE,
    };

    private static readonly TiffTag[] ScalarDoubleTags =
    {
        TiffTag.XRESOLUTION,    // DPI directly affects OCR accuracy
        TiffTag.YRESOLUTION,
    };

    /// <summary>
    /// Lazily yields each page of <paramref name="inputPath"/> as a standalone
    /// single-page TIFF. The source file stays open for the lifetime of the
    /// enumeration and only one page is materialised at a time, so peak memory
    /// is roughly one page rather than the whole file.
    /// </summary>
    public static IEnumerable<byte[]> SplitTiffToPages(string inputPath)
    {
        using (Tiff input = Tiff.Open(inputPath, "r"))
        {
            if (input == null)
                throw new InvalidOperationException($"Could not open TIFF: {inputPath}");

            int pageCount = input.NumberOfDirectories();
            for (int page = 0; page < pageCount; page++)
            {
                input.SetDirectory((short)page);
                yield return ExtractCurrentPage(input);
            }
        }
    }

    private static byte[] ExtractCurrentPage(Tiff input)
    {
        using (var ms = new MemoryStream())
        {
            // Default TiffStream operates on the MemoryStream passed as clientData.
            using (Tiff output = Tiff.ClientOpen("InMemory", "w", ms, new TiffStream()))
            {
                if (output == null)
                    throw new InvalidOperationException("Could not create in-memory TIFF.");

                CopyTags(input, output);

                if (input.IsTiled())
                    CopyRawTiles(input, output);
                else
                    CopyRawStrips(input, output);

                output.WriteDirectory();
            }

            return ms.ToArray();
        }
    }

    private static void CopyTags(Tiff input, Tiff output)
    {
        foreach (TiffTag tag in ScalarIntTags)
        {
            FieldValue[] v = input.GetField(tag);
            if (v != null && v.Length > 0)
                output.SetField(tag, v[0].ToInt());
        }
        foreach (TiffTag tag in ScalarDoubleTags)
        {
            FieldValue[] v = input.GetField(tag);
            if (v != null && v.Length > 0)
                output.SetField(tag, v[0].ToDouble());
        }
        // Strip vs tile layout must match the raw data exactly, otherwise the
        // raw bytes won't line up with the declared boundaries.
        if (input.IsTiled())
        {
            output.SetField(TiffTag.TILEWIDTH, input.GetField(TiffTag.TILEWIDTH)[0].ToInt());
            output.SetField(TiffTag.TILELENGTH, input.GetField(TiffTag.TILELENGTH)[0].ToInt());
        }
        else
        {
            FieldValue[] rps = input.GetField(TiffTag.ROWSPERSTRIP);
            if (rps != null && rps.Length > 0)
                output.SetField(TiffTag.ROWSPERSTRIP, rps[0].ToInt());
        }

        // Palette images: the colour map is required to interpret pixel indices.
        FieldValue[] cmap = input.GetField(TiffTag.COLORMAP);
        if (cmap != null && cmap.Length >= 3)
            output.SetField(TiffTag.COLORMAP,
                cmap[0].ToShortArray(), cmap[1].ToShortArray(), cmap[2].ToShortArray());
    }

    private static void CopyRawStrips(Tiff input, Tiff output)
    {
        int stripCount = input.NumberOfStrips();
        int[] byteCounts = input.GetField(TiffTag.STRIPBYTECOUNTS)[0].ToIntArray();
        for (int strip = 0; strip < stripCount; strip++)
        {
            byte[] buffer = new byte[byteCounts[strip]];
            int read = input.ReadRawStrip(strip, buffer, 0, buffer.Length);
            output.WriteRawStrip(strip, buffer, read);
        }
    }

    private static void CopyRawTiles(Tiff input, Tiff output)
    {
        int tileCount = input.NumberOfTiles();
        int[] byteCounts = input.GetField(TiffTag.TILEBYTECOUNTS)[0].ToIntArray();
        for (int tile = 0; tile < tileCount; tile++)
        {
            byte[] buffer = new byte[byteCounts[tile]];
            int read = input.ReadRawTile(tile, buffer, 0, buffer.Length);
            output.WriteRawTile(tile, buffer, read);
        }
    }

    /// <summary>
    /// Lazily yields multi-page TIFF chunks (the equivalent of the old
    /// Magick.NET 100-pages-per-chunk approach). A new chunk is started when
    /// adding the next page would push the chunk past
    /// <paramref name="maxChunkBytes"/>, or when <paramref name="maxPagesPerChunk"/>
    /// is reached - whichever comes first.
    ///
    /// Size is the real guard: page count alone can exceed the 2 GB AnyBitmap
    /// limit on large pages. The byte total here is the encoded (compressed)
    /// size, which is a cheap proxy - validate the cap against your actual
    /// pages, since decoded size can be much larger than encoded.
    /// </summary>
    public static IEnumerable<byte[]> SplitTiffToChunks(
        string inputPath,
        int maxPagesPerChunk = 100,
        long maxChunkBytes = 1_500_000_000L)
    {
        using (Tiff input = Tiff.Open(inputPath, "r"))
        {
            if (input == null)
                throw new InvalidOperationException($"Could not open TIFF: {inputPath}");
            int pageCount = input.NumberOfDirectories();
            int page = 0;
            while (page < pageCount)
            {
                using (var ms = new MemoryStream())
                {
                    using (Tiff output = Tiff.ClientOpen("InMemory", "w", ms, new TiffStream()))
                    {
                        if (output == null)
                            throw new InvalidOperationException("Could not create in-memory TIFF.");

                        int pagesInChunk = 0;
                        long chunkBytes = 0;

                        while (page < pageCount && pagesInChunk < maxPagesPerChunk)
                        {
                            input.SetDirectory((short)page);
                            long pageBytes = RawPageByteSize(input);
                            // Stop before exceeding the cap, but always allow at
                            // least one page so a single large page still goes through.
                            if (pagesInChunk > 0 && chunkBytes + pageBytes > maxChunkBytes)
                                break;
                            CopyTags(input, output);
                            if (input.IsTiled())
                                CopyRawTiles(input, output);
                            else
                                CopyRawStrips(input, output);

                            output.WriteDirectory(); // finalise this page as one directory in the chunk
                            chunkBytes += pageBytes;
                            pagesInChunk++;
                            page++;
                        }
                    }

                    yield return ms.ToArray();
                }
            }
        }
    }

    private static long RawPageByteSize(Tiff page)
    {
        TiffTag tag = page.IsTiled() ? TiffTag.TILEBYTECOUNTS : TiffTag.STRIPBYTECOUNTS;
        int[] counts = page.GetField(tag)[0].ToIntArray();
        long total = 0;
        foreach (int c in counts)
            total += c;
        return total;
    }
}
C#

次のページが総量をSplitTiffToChunksは新しいチャンクを開始します。制限を超える単一ページは常に単独で許可されます。

3. 各チャンクをOCR

ファイルパスではなくSplitTiffToChunksを反復します。

var inputPath = "2gb_benchmark1200.tiff";
var ocr = new IronTesseract();
int chunk = 0;
foreach (byte[] chunkBytes in TiffPageSplitter.SplitTiffToChunks(inputPath, maxPagesPerChunk: 100))
{
    using (var ocrInput = new OcrInput())
    {
        ocrInput.LoadImage(chunkBytes); // loads every page in the chunk
        var result = ocr.Read(ocrInput);
        Console.WriteLine($"Chunk {chunk}: {result.Text?.Length ?? 0} chars");
    }
    chunk++;
}
C#

バイト配列の渡しにより、各入力は制限内に保たれます。OcrInput.LoadImage(filePath)は現在、ファイルが大きすぎる場合には明確なエラーを投げずにロードされたページがゼロで返されますので注意してください; この沈黙した失敗は既知の問題であり、チャンク化はそれを完全に回避します。

4. データに合わせたチャンク制限の調整

あなたのTIFFに合わせてmaxChunkBytesを調整します。 もしデコード後にチャンクが2 GBに近づくか、メモリが逼迫している場合はこれを下げます; オーバーヘッドを削減するために小さいページの場合はページ数を増やします。

警告: maxChunkBytesはエンコードされた(圧縮された)サイズに対して測定され、これは安価なプロキシでしかありません。 デコードされたサイズははるかに大きくなる可能性があるため、非常に大きな単一のページがデコードされた後に2 GBを超えることがあります。 デフォルトの1.5 GBは制限内に余裕を持たせます。

注意事項と制限

  • タグのカバレッジ: スプリッターはScalarDoubleTagsにリストされたタグのみを引き継ぎます。 TIFFがICCプロファイルやアルファチャネル用のEXTRASAMPLESなどでカバーされていないタグを使用している場合は、これらのリストを拡張するか、生コピーされたバイトが誤解釈される可能性があります。
  • 管理された依存関係: LibTiff.NETは完全に管理されており、Magick.NETとは異なり、パッケージサイズやデプロイメントのフットプリントを増やすImageMagickネイティブライブラリを搭載していません。
  • 正確な保存: 生のストリップまたはタイルコピーは、Magick.NETアプローチが行うデコードと再エンコードを避け、元の圧縮とピクセルデータをそのまま保持します。

さらに読むには、BitMiracle.LibTiff.NET on NuGetをご覧ください。

Curtis Chau
テクニカルライター

Curtis Chauは、カールトン大学でコンピュータサイエンスの学士号を取得し、Node.js、TypeScript、JavaScript、およびReactに精通したフロントエンド開発を専門としています。直感的で美しいユーザーインターフェースを作成することに情熱を持ち、Curtisは現代のフレームワークを用いた開発や、構造の良い視覚的に魅力的なマニュアルの作成を楽しんでいます。

...
詳しく読む

準備はできましたか?

Nuget Downloads 6,236,385バージョン:2026.9リリースされたばかり

あなたの無料30日間の試用キーをすぐに入手。
クレジットカードやアカウントの作成は不要です。
PDF用C# NuGetライブラリ
NuGetでインストール

バージョン: 2026.9

PM > Install-Package IronOcr
nuget.org/packages/IronOcr/
  1. ソリューションエクスプローラーで参照を右クリックし、NuGetパッケージを管理を選択
  2. 「参照」を選択して「IronOCR」を検索
  3. パッケージを選択してインストール
C# PDF DLL
DLLをダウンロード

バージョン: 2026.9

または、Windowsインストーラーをこちらからダウンロード。

  1. IronOCRをダウンロードして、ソリューションディレクトリ内の~/Libsなどの場所に解凍します
  2. Visual Studioソリューションエクスプローラーで、リファレンスを右クリックします。「参照」、「IronOCR.dll」を選択

$999からのライセンス

Key in blue circle

無料の30日間トライアルキーをすぐに入手してください。

Your trial license will be sent to your email address

制限なし。100% ロック解除済み。クレジットカード不要。

bullet_checkedクレジットカードやアカウントの作成は不要です。制限なし。100% ロック解除済み。クレジットカード不要。
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried IronPDF
義務のない相談を受ける
下記のフォームを記入するか、sales@ironsoftware.comにメールしてください。
あなたの詳細は常に守秘されます。
世界中の数百万人のエンジニアから信頼されています。
ライセンスはより安く
あなたの無料30日間の試用キーをすぐに入手。
クレジットカードやアカウントの作成は不要です。