Tools,

Tesseract in C# on Mac and Linux, tested on 55 scans

2.6x

less CPU with one engine

Fix the setup

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

CPU seconds, Tesseract 5.5.4
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.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one 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.