IRONSOFTWAREHOME

OCR de TIFFs acima de 2 GB com LibTiff.NET

Curtis Chau
Curtis Chau
Updated: 3 de julho de 2026

IronOCR carrega cada imagem em um buffer em memória AnyBitmap indexado por um inteiro de 32 bits, então ele se limita a aproximadamente 2 GB, independentemente de quanto de memória do sistema está disponível. Um TIFF maior do que isso falha ao carregar. Divida o arquivo superdimensionado em partes menores que 2 GB com BitMiracle.LibTiff.NET e faça OCR em cada parte.

Esta é uma alternativa totalmente gerenciada ao ajuste do Magick.NET: ele copia páginas no nível de faixa ou bloco cru, sem etapa de decodificação ou re-encode.

Observe: O limite de 2 GB é uma limitação arquitetônica do AnyBitmap, não um problema do sistema operacional ou de memória. O streaming TIFF por página nativo dentro do IronOCR ainda não está disponível.

Solução

Antes de começar, certifique-se de ter o IronOCR (IronTesseract) em um projeto .NET. Os tipos LibTiff usados abaixo estão no namespace BitMiracle.LibTiff.Classic.

1. Adicione o Pacote LibTiff.NET

dotnet add package BitMiracle.LibTiff.NET
SHELL

2. Adicione o Assistente TiffPageSplitter

O assistente copia cada página no nível de faixa ou bloco cru, então os dados de pixels e a compressão são preservados exatamente. Ele transmite páginas uma de cada vez, mantendo o uso máximo de memória em aproximadamente uma parte em vez do arquivo todo, e produz partes de várias páginas que ficam abaixo do limite de tamanho.

/// <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 inicia uma nova parte sempre que a próxima página empurraria o total além de maxChunkBytes ou uma vez que maxPagesPerChunk seja alcançado, o que ocorrer primeiro. Uma única página maior que o limite sempre é permitida isoladamente.

3. Faça OCR em Cada Parte

Itere SplitTiffToChunks e carregue cada array de bytes com OcrInput.LoadImage(byte[]) em vez do caminho do arquivo, para que nada maior do que o limite chegue a 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#

Passar o array de bytes mantém cada entrada abaixo do limite. Note que OcrInput.LoadImage(filePath) atualmente retorna zero páginas carregadas em vez de lançar um erro claro quando o arquivo é muito grande; essa falha silenciosa é um problema conhecido, e a divisão o contorna completamente.

4. Ajuste os Limites de Partes para Seus Dados

Ajuste maxPagesPerChunk e maxChunkBytes para corresponder aos seus TIFFs. Reduza-os se uma parte se aproximar de 2 GB quando decodificada ou se a memória estiver apertada; aumente a contagem de páginas para páginas menores para reduzir o overhead.

Aviso: maxChunkBytes é medido contra o tamanho codificado (comprimido), que é apenas uma aproximação barata. O tamanho decodificado pode ser muito maior, então uma única página muito grande ainda pode exceder 2 GB uma vez decodificada. O padrão de 1,5 GB deixa margem de segurança abaixo do limite.

Notas e Limitações

  • Suporte a tags: o divisor carrega apenas as tags listadas em ScalarIntTags e ScalarDoubleTags. Se seus TIFFs usarem tags não cobertas ali, como perfis ICC ou EXTRASAMPLES para canais alfa, estenda essas listas ou os bytes copiados em bruto podem ser mal interpretados.
  • Dependência gerenciada: LibTiff.NET é totalmente gerenciada sem binários nativos, diferentemente do Magick.NET, que envia bibliotecas nativas ImageMagick que aumentam o tamanho do pacote e a pegada de implantação.
  • Preservação exata: a cópia de faixa ou bloco cru evita a decodificação e o re-encode que a abordagem Magick.NET executa, mantendo a compressão original e os dados de pixels intactos.

For further reading, see BitMiracle.LibTiff.NET on NuGet.

Curtis Chau
Redator Técnico

Curtis Chau é bacharel em Ciência da Computação (Universidade Carleton) e se especializa em desenvolvimento front-end, com experiência em Node.js, TypeScript, JavaScript e React. Apaixonado por criar interfaces de usuário intuitivas e esteticamente agradáveis, Curtis gosta de trabalhar com frameworks modernos e criar manuais bem estruturados e visualmente atraentes.

...
Leia mais

Pronto para começar?

Nuget Downloads 6,236,385Versão:2026.9recém-lançado

Obtenha sua chave de avaliação gratuita de 30 dias instantaneamente.
Não é necessário cartão de crédito nem criação de conta.
Biblioteca NuGet C# para PDF
Instalar com NuGet

Versão: 2026.9

PM > Install-Package IronOcr
nuget.org/packages/IronOcr/
  1. No Solution Explorer, clique com o botão direito do mouse em Referências e selecione Gerenciar Pacotes NuGet.
  2. Selecione Procurar e pesquise "IronOCR".
  3. Selecione o pacote e instale.
DLL de PDF em C#
Baixar DLL

Versão: 2026.9

ou faça o download do Windows Installer aqui .

  1. Baixe e descompacte o IronOCR em um local como ~/Libs dentro do diretório da sua solução.
  2. No Solution Explorer do Visual Studio, clique com o botão direito do mouse em Referências. Selecione Procurar e digite "IronOCR.dll".

Licenças a partir de US$ 749

Key in blue circle

Obtenha sua chave de avaliação gratuita de 30 dias instantaneamente.

Your trial license will be sent to your email address

Sem limitações. 100% desbloqueado. Sem cartão de crédito.

bullet_checkedNão é necessário cartão de crédito nem criação de conta.Sem limitações. 100% desbloqueado. Sem cartão de crédito.
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried IronPDF
Agende sua consulta sem compromisso.
Preencha o formulário abaixo ou envie um e-mail para sales@ironsoftware.com
Os seus dados serão sempre mantidos em sigilo.
Aprovado por milhões de engenheiros em todo o mundo.
Logotipos dos clientes da Iron Software
Obtenha sua chave de avaliação gratuita de 30 dias instantaneamente.
Não é necessário cartão de crédito nem criação de conta.