使用 LibTiff.NET 对超过 2 GB 的 TIFF 进行 OCR
IronOCR 将每个图像加载到由 32 位整数索引的内存中AnyBitmap缓冲区,因此无论系统可用内存多少,都将限制在大约 2 GB。 大于这个大小的 TIFF 无法加载。 使用 BitMiracle.LibTiff.NET 将超大文件拆分成小于 2 GB 的块,并对每个块进行 OCR。
这是 Magick.NET 替代方案的全面管理版本:在原始条带或瓦片级别复制页面,无需解码或重新编码步骤。
解决方案
开始之前,确保在一个 .NET 项目中包含 IronOCR (IronTesseract)。 下面使用的 LibTiff 类型位于 BitMiracle.LibTiff.Classic 命名空间中。
1. 添加 LibTiff.NET 包
dotnet add package BitMiracle.LibTiff.NET
dotnet add package BitMiracle.LibTiff.NET
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;
}
}
/// <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;
}
}
Imports System
Imports System.Collections.Generic
Imports System.IO
''' <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 NotInheritable 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 Shared ReadOnly ScalarIntTags As TiffTag() = {
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 Shared ReadOnly ScalarDoubleTags As TiffTag() = {
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 Shared Iterator Function SplitTiffToPages(inputPath As String) As IEnumerable(Of Byte())
Using input As Tiff = Tiff.Open(inputPath, "r")
If input Is Nothing Then
Throw New InvalidOperationException($"Could not open TIFF: {inputPath}")
End If
Dim pageCount As Integer = input.NumberOfDirectories()
For page As Integer = 0 To pageCount - 1
input.SetDirectory(CShort(page))
Yield ExtractCurrentPage(input)
Next
End Using
End Function
Private Shared Function ExtractCurrentPage(input As Tiff) As Byte()
Using ms As New MemoryStream()
' Default TiffStream operates on the MemoryStream passed as clientData.
Using output As Tiff = Tiff.ClientOpen("InMemory", "w", ms, New TiffStream())
If output Is Nothing Then
Throw New InvalidOperationException("Could not create in-memory TIFF.")
End If
CopyTags(input, output)
If input.IsTiled() Then
CopyRawTiles(input, output)
Else
CopyRawStrips(input, output)
End If
output.WriteDirectory()
End Using
Return ms.ToArray()
End Using
End Function
Private Shared Sub CopyTags(input As Tiff, output As Tiff)
For Each tag As TiffTag In ScalarIntTags
Dim v As FieldValue() = input.GetField(tag)
If v IsNot Nothing AndAlso v.Length > 0 Then
output.SetField(tag, v(0).ToInt())
End If
Next
For Each tag As TiffTag In ScalarDoubleTags
Dim v As FieldValue() = input.GetField(tag)
If v IsNot Nothing AndAlso v.Length > 0 Then
output.SetField(tag, v(0).ToDouble())
End If
Next
' 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() Then
output.SetField(TiffTag.TILEWIDTH, input.GetField(TiffTag.TILEWIDTH)(0).ToInt())
output.SetField(TiffTag.TILELENGTH, input.GetField(TiffTag.TILELENGTH)(0).ToInt())
Else
Dim rps As FieldValue() = input.GetField(TiffTag.ROWSPERSTRIP)
If rps IsNot Nothing AndAlso rps.Length > 0 Then
output.SetField(TiffTag.ROWSPERSTRIP, rps(0).ToInt())
End If
End If
' Palette images: the colour map is required to interpret pixel indices.
Dim cmap As FieldValue() = input.GetField(TiffTag.COLORMAP)
If cmap IsNot Nothing AndAlso cmap.Length >= 3 Then
output.SetField(TiffTag.COLORMAP,
cmap(0).ToShortArray(), cmap(1).ToShortArray(), cmap(2).ToShortArray())
End If
End Sub
Private Shared Sub CopyRawStrips(input As Tiff, output As Tiff)
Dim stripCount As Integer = input.NumberOfStrips()
Dim byteCounts As Integer() = input.GetField(TiffTag.STRIPBYTECOUNTS)(0).ToIntArray()
For strip As Integer = 0 To stripCount - 1
Dim buffer As Byte() = New Byte(byteCounts(strip) - 1) {}
Dim read As Integer = input.ReadRawStrip(strip, buffer, 0, buffer.Length)
output.WriteRawStrip(strip, buffer, read)
Next
End Sub
Private Shared Sub CopyRawTiles(input As Tiff, output As Tiff)
Dim tileCount As Integer = input.NumberOfTiles()
Dim byteCounts As Integer() = input.GetField(TiffTag.TILEBYTECOUNTS)(0).ToIntArray()
For tile As Integer = 0 To tileCount - 1
Dim buffer As Byte() = New Byte(byteCounts(tile) - 1) {}
Dim read As Integer = input.ReadRawTile(tile, buffer, 0, buffer.Length)
output.WriteRawTile(tile, buffer, read)
Next
End Sub
''' <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 Shared Iterator Function SplitTiffToChunks(
inputPath As String,
Optional maxPagesPerChunk As Integer = 100,
Optional maxChunkBytes As Long = 1500000000L) As IEnumerable(Of Byte())
Using input As Tiff = Tiff.Open(inputPath, "r")
If input Is Nothing Then
Throw New InvalidOperationException($"Could not open TIFF: {inputPath}")
End If
Dim pageCount As Integer = input.NumberOfDirectories()
Dim page As Integer = 0
While page < pageCount
Using ms As New MemoryStream()
Using output As Tiff = Tiff.ClientOpen("InMemory", "w", ms, New TiffStream())
If output Is Nothing Then
Throw New InvalidOperationException("Could not create in-memory TIFF.")
End If
Dim pagesInChunk As Integer = 0
Dim chunkBytes As Long = 0
While page < pageCount AndAlso pagesInChunk < maxPagesPerChunk
input.SetDirectory(CShort(page))
Dim pageBytes As Long = 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 AndAlso chunkBytes + pageBytes > maxChunkBytes Then
Exit While
End If
CopyTags(input, output)
If input.IsTiled() Then
CopyRawTiles(input, output)
Else
CopyRawStrips(input, output)
End If
output.WriteDirectory() ' finalise this page as one directory in the chunk
chunkBytes += pageBytes
pagesInChunk += 1
page += 1
End While
End Using
Yield ms.ToArray()
End Using
End While
End Using
End Function
Private Shared Function RawPageByteSize(page As Tiff) As Long
Dim tag As TiffTag = If(page.IsTiled(), TiffTag.TILEBYTECOUNTS, TiffTag.STRIPBYTECOUNTS)
Dim counts As Integer() = page.GetField(tag)(0).ToIntArray()
Dim total As Long = 0
For Each c As Integer In counts
total += c
Next
Return total
End Function
End Class
SplitTiffToChunks 在下一个页面将推动总量超出 maxChunkBytes 或达到 maxPagesPerChunk 时开始新块,以先到者为准。单页超出上限的总是可以单独通过。
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++;
}
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++;
}
Imports IronOcr
Dim inputPath As String = "2gb_benchmark1200.tiff"
Dim ocr As New IronTesseract()
Dim chunk As Integer = 0
For Each chunkBytes As Byte() In TiffPageSplitter.SplitTiffToChunks(inputPath, maxPagesPerChunk:=100)
Using ocrInput As New OcrInput()
ocrInput.LoadImage(chunkBytes) ' loads every page in the chunk
Dim result = ocr.Read(ocrInput)
Console.WriteLine($"Chunk {chunk}: {If(result.Text?.Length, 0)} chars")
End Using
chunk += 1
Next
传递字节数组将每个输入保持在上限之内。请注意 OcrInput.LoadImage(filePath) 目前在文件过大时返回零加载页面,而不是抛出明确错误; 这默默失败是一个已知问题,而分块完全绕过了它。
4. 根据您的数据调整块限制
调整 maxPagesPerChunk 和 maxChunkBytes 以匹配您的 TIFF。 如果解码后的块接近 2 GB 或内存紧张,请降低它们; 增加较小页面的页数以减少开销。
注意事项和限制
- 标签覆盖:拆分器仅携带
ScalarIntTags和ScalarDoubleTags中列出的标签。 如果您的 TIFF 使用了未涵盖的标签,例如用于 alpha 通道的 ICC 配置文件或EXTRASAMPLES,请扩展这些列表,否则原始复制的字节可能会被误解。 - 托管依赖:与 Magick.NET 不同,LibTiff.NET 是完全托管的,无需本机二进制文件,而 Magick.NET 则提供会增加包大小和部署影响的 ImageMagick 本机库。
- 精确保留:原始条带或瓦片复制避免了 Magick.NET 方法执行的解码和重新编码,保留了原始压缩和像素数据。
欲了解更多信息,请参阅 NuGet 上的 BitMiracle.LibTiff.NET。

