IRONSOFTWAREHOME

LibTiff.NET을 사용하여 2GB 이상의 TIFF OCR

Curtis Chau
Curtis Chau
Updated: 2026년 7월 3일

IronOCR는 이미지를 32비트 정수로 인덱싱된 AnyBitmap 메모리 버퍼에 각각 로드하여, 시스템 메모리가 얼마나 사용 가능한지와 관계없이 2GB에서 제한됩니다. 그보다 큰 TIFF는 로드되지 않습니다. 초과 크기의 파일을 BitMiracle.LibTiff.NET로 2GB 미만의 청크로 나누고 각 청크를 OCR 처리합니다.

이는 Magick.NET 우회에 대한 완전 관리 대안입니다: 해석하거나 다시 인코딩하는 단계 없이 원시 스트립 또는 타일 수준에서 페이지를 복사합니다.

참고해 주세요: 2GB 제한은 OS 또는 메모리 문제가 아닌 AnyBitmap의 구조적 한계입니다. IronOCR 내의 네이티브 페이지별 TIFF 스트리밍은 아직 제공되지 않습니다.

해결책

시작하기 전에 .NET 프로젝트에 IronOCR (IronTesseract)가 있는지 확인하세요. 아래에서 사용한 LibTiff 타입은 BitMiracle.LibTiff.Classic 네임스페이스에 존재합니다.

1. LibTiff.NET 패키지 추가

dotnet add package BitMiracle.LibTiff.NET
SHELL

2. TiffPageSplitter 헬퍼 추가

헬퍼는 원시 스트립 또는 타일 수준에서 각 페이지를 복사하여, 픽셀 데이터와 압축을 그대로 유지합니다. 페이지를 한 번에 하나씩 스트리밍하므로 피크 메모리가 전체 파일이 아닌 대략 하나의 청크로 유지되며, 각 청크가 사이즈 제한 이내에 머무는 다중 페이지 청크를 생성합니다.

/// <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#

다음 페이지가 maxChunkBytes를 초과할 때 또는 maxPagesPerChunk에 도달할 때마다 SplitTiffToChunks는 새로운 청크를 시작합니다. 제한보다 큰 단일 페이지는 항상 개별적으로 허용됩니다.

3. 각 청크에 대한 OCR

SplitTiffToChunks을 반복하여, 파일 경로 대신 OcrInput.LoadImage(byte[])으로 각 바이트 배열을 로드하여 한도가 넘는 것이 AnyBitmap에 도달하지 않도록 합니다.

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)는 파일이 너무 클 때 명확한 오류를 던지는 대신 로드된 페이지 수를 0으로 반환합니다; 이러한 조용한 실패는 알려진 문제이며, 청크 처리를 통해 이를 완전히 회피합니다.

4. 데이터에 맞게 청크 제한 조정

TIFF에 맞게 maxPagesPerChunkmaxChunkBytes를 조정합니다. 디코딩 후 청크가 2GB에 근접하거나 메모리가 부족한 경우 하향 조정하십시오; 오버헤드를 줄이기 위해 작은 페이지의 페이지 수를 증가시킵니다.

경고: maxChunkBytes는 인코딩(압축)된 크기 기준으로 측정되며, 이는 저렴한 대리일 뿐입니다. 디코딩된 크기는 훨씬 클 수 있으므로, 아주 큰 단일 페이지는 디코딩 후에도 여전히 2GB를 초과할 수 있습니다. 기본값 1.5GB는 제한 아래 여유 공간을 남깁니다.

참고 사항 및 제한 사항

  • 태그 적용 범위: 스플리터는 ScalarIntTagsScalarDoubleTags에 나열된 태그만 이월합니다. TIFF에 ICC 프로파일이나 알파 채널용 EXTRASAMPLES 같은 태그가 있는 경우, 해당 목록을 확장하거나 원시 복사된 바이트가 잘못 해석될 수 있습니다.
  • 관리되는 종속성: LibTiff.NET은 관리된 상태로, Magick.NET처럼 이미지매직 네이티브 라이브러리를 포함하지 않아 패키지 크기와 배포 발자국을 증가시키지 않습니다.
  • 정확한 보존: 원시 스트립 또는 타일 복사는 Magick.NET 접근 방식이 수행하는 디코드 및 재인코딩을 피하며, 원래의 압축 및 픽셀 데이터를 그대로 유지합니다.

자세한 내용은 NuGet에서 BitMiracle.LibTiff.NET을 참조하세요.

Curtis Chau
기술 문서 작성자

커티스 차우는 칼턴 대학교에서 컴퓨터 과학 학사 학위를 취득했으며, Node.js, TypeScript, JavaScript, React를 전문으로 하는 프론트엔드 개발자입니다. 직관적이고 미적으로 뛰어난 사용자 인터페이스를 만드는 데 열정을 가진 그는 최신 프레임워크를 활용하고, 잘 구성되고 시각적으로 매력적인 매뉴얼을 제작하는 것을 즐깁니다.

...
더 읽어보기

시작할 준비 되셨나요?

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"을 선택합니다.

라이선스 가격은 749달러 부터 시작합니다.

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으로 이메일을 보내주세요.
고객님의 정보는 항상 비밀로 유지됩니다.
전 세계 수백만 엔지니어들이 신뢰하는 제품입니다.
Iron Software의 고객 로고
지금 바로 30일 무료 체험판 키를 받으세요.
신용카드나 계정 생성은 필요하지 않습니다.