Tesseract in C# on Mac and Linux, tested on 55 scans
2.6x
less CPU with one engine
Failed to find library: add two links
The NuGet package on a Mac, out of the box:
System.DllNotFoundException: Failed to find library "libleptonica-1.82.0.dylib" for platform x64.
The package carries Tesseract for Windows only. On Mac and Linux it looks in an x64 folder next to your app, for two fixed names, even on ARM.
| System | Install | Names in x64/ |
|---|---|---|
| Mac | brew install tesseract |
libleptonica-1.82.0.dylib, libtesseract50.dylib |
| Ubuntu 24.04 | apt-get install tesseract-ocr |
libleptonica-1.82.0.so, libtesseract50.so |
On a Mac, add this to your .csproj. Each build copies Homebrew's files in under those names:
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<None Include="/opt/homebrew/lib/libleptonica.dylib" Link="x64/libleptonica-1.82.0.dylib" CopyToOutputDirectory="PreserveNewest" />
<None Include="/opt/homebrew/lib/libtesseract.dylib" Link="x64/libtesseract50.dylib" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Linux: then the libdl error
With the two links in place, Ubuntu stops one step later:
System.DllNotFoundException: Unable to load shared library 'libdl' or one of its dependencies.
The wrapper asks for libdl by its bare name. The .NET runtime image has libdl.so.2 but no libdl.so. A third link next to your app fixes it. This Dockerfile makes all three:
FROM mcr.microsoft.com/dotnet/runtime:10.0
RUN apt-get update && apt-get install -y --no-install-recommends tesseract-ocr
COPY bin/Release/net10.0/ /app/
RUN L=/usr/lib/$(uname -m)-linux-gnu && cd /app && \
ln -s $L/liblept.so.5 x64/libleptonica-1.82.0.so && \
ln -s $L/libtesseract.so.5 x64/libtesseract50.so && \
ln -s $L/libdl.so.2 libdl.so
ENTRYPOINT ["dotnet", "/app/Ocr.dll"]
Point at the tessdata folder itself
The wrapper's notes say to pass the folder above tessdata. With Tesseract 5.5.4, that fails:
Error opening data file /opt/homebrew/share/eng.traineddata
Tesseract.TesseractException: Failed to initialise tesseract engine.. See https://github.com/charlesw/tesseract/wiki/Error-1 for details.
Pass the tessdata folder: /opt/homebrew/share/tessdata on
a Mac, /usr/share/tesseract-ocr/5/tessdata on Ubuntu.
The code, run on 55 scans
dotnet add package Tesseract --version 5.2.0
using Tesseract;
var tessdata = OperatingSystem.IsMacOS()
? "/opt/homebrew/share/tessdata"
: "/usr/share/tesseract-ocr/5/tessdata";
using var engine = new TesseractEngine(tessdata, "eng", EngineMode.Default);
engine.SetVariable("user_defined_dpi", "300");
using var img = Pix.LoadFromFile("scan.jpg");
using var page = engine.Process(img);
Console.Write(page.GetText());
Run as printed, it wrote the tesseract command's text on
55 of 55 scans on the Mac. On Ubuntu, from the Dockerfile above, it
matched apt's command on 5 of 5.
Set the dpi to match the command
My scans have no dpi in the file, so without that line Tesseract guesses it. The page mode needs nothing: the wrapper's default is psm 3, like the command's.
40 of 55 scans, the command's text without it
13,855 words found, 3 fewer
Same text, about the same CPU
55 of 55 scans, the same text
13,858 of 14,693 words found, both
| Job | Command | C# |
|---|---|---|
| 55 full pages | 153.9 s | 145.2 s |
| 259 line images | 49.8 s | 42.5 s |
The command starts once per image. The C# pages run kept one engine, and the lines run made a new engine per image.
Many small images: keep one engine
Each new TesseractEngine loads the model again. Make one, then:
foreach (var file in files)
{
using var img = Pix.LoadFromFile(file);
using var page = engine.Process(img, PageSegMode.SingleLine);
Console.Write(page.GetText());
}
16.4 s for 259 lines, not 42.5 s
All 259 lines, the command's text
PDFs: render each page first
System.IO.IOException: Failed to load image 'scan.pdf'.
Leptonica reads images, not PDFs. I rendered each page at 300 dpi with the PDFtoImage package (5.4.0), then passed it on:
var pdf = File.ReadAllBytes("scan.pdf");
foreach (var bmp in Conversion.ToImages(pdf, options: new RenderOptions(Dpi: 300)))
{
using (bmp)
using (var png = bmp.Encode(SKEncodedImageFormat.Png, 100))
using (var img = Pix.LoadFromMemory(png.ToArray()))
using (var page = engine.Process(img))
Console.Write(page.GetText());
}
1,881 of 1,955 words, 3 more than the JPEGs
5.2 s of CPU a page, not 2.6 s
How I tested it
- Pages: 55 proofread Wikisource scans from 11 printed books, JPEG, 1,920 pixels wide, no dpi in the file. The same set as Tess4J.
- Score: true words found in order, with the scorer of the tessdata test.
- Apple Silicon Mac, .NET SDK 10.0.404, Tesseract NuGet 5.2.0, Homebrew Tesseract 5.5.4 and Leptonica 1.87.0, tessdata_fast English.
- Ubuntu: Docker, mcr.microsoft.com/dotnet/runtime:10.0 (Ubuntu 24.04) on arm64, apt's Tesseract 5.3.4 and Leptonica 1.82.0.
- Time is CPU time of the whole process tree, .NET start included, on the Mac's efficiency cores. One round each.
- PDF: the first 10 scans in one PDF, JPEG bytes kept, page size for 300 dpi.
What I did not test
- Windows, Intel Macs, x86-64 Linux, and other distributions.
- The TesseractOCR fork and older package versions.
- The PDF code on Linux, threads, and wall time.
- Other languages, tessdata_best, and photos.
Sources
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Does the NuGet package include Tesseract for Mac or Linux?
No. It carries tesseract50.dll and leptonica-1.82.0.dll for 32-bit and 64-bit Windows. On Mac and Linux, it used Homebrew's Tesseract 5.5.4 and Ubuntu's 5.3.4 under the names it asks for.
Does it give the same text as the command?
Yes, byte for byte on 55 of 55 scans, with the same Tesseract, the same model, psm 3 and 300 dpi on both.
Which page mode does it use?
Auto, which is psm 3, the same as the tesseract command. Pass a PageSegMode to Process to change it, as in the line code above.