The Tesseract C++ API, tested on 55 scans
50.8%
of two-column text in order, by default
The code, run on 55 scans
Install the library first: brew install tesseract on a
Mac, the apt packages libtesseract-dev and
tesseract-ocr-eng on Debian.
#include <allheaders.h>
#include <tesseract/baseapi.h>
#include <cstdio>
int main(int argc, char **argv) {
tesseract::TessBaseAPI api;
if (api.Init(nullptr, "eng")) return 1;
api.SetPageSegMode(tesseract::PSM_AUTO);
for (int i = 1; i < argc; i++) {
Pix *pix = pixRead(argv[i]);
if (!pix) return 1;
api.SetImage(pix);
api.SetSourceResolution(300);
char *text = api.GetUTF8Text();
printf("%s", text);
delete[] text;
pixDestroy(&pix);
}
api.End();
}
clang++ -std=c++17 main.cpp -o ocr $(pkg-config --cflags --libs tesseract lept)
55 of 55 scans, the command's text
13,858 of 14,693 words found, both
Two build errors, two fixes
Tesseract's own example includes
<leptonica/allheaders.h>. With Homebrew's
pkg-config flags:
fatal error: 'leptonica/allheaders.h' file not found
Include <allheaders.h>. It built on Debian too.
Leave out lept, and the link fails on both:
Undefined symbols for architecture arm64:
"_pixDestroy", referenced from:
pkg-config --libs tesseract lists no Leptonica, the home
of pixRead and pixDestroy.
CMake: pkg-config, not find_package
find_package(Tesseract) failed on Homebrew and on Debian:
neither installs a CMake config file. This worked on both:
cmake_minimum_required(VERSION 3.16)
project(ocr CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig REQUIRED)
pkg_check_modules(TESS REQUIRED IMPORTED_TARGET tesseract lept)
add_executable(ocr main.cpp)
target_link_libraries(ocr PkgConfig::TESS)
Set page mode 3: the API's is 6
The command reads with page mode 3, automatic layout. The API starts at 6, one block of text. On book pages that changed little. On two columns, it read straight across.
9 of 55 book scans, the command's text
50.8% of letters in order on 30 two-column pages, not 99.2%
Set the dpi after SetImage
My scans have no dpi in the file. Call
SetSourceResolution after SetImage: a call
before it was lost.
55 of 55 scans, the command's text, called after
40 of 55 called before, the same as never
Setting the variable user_defined_dpi to 300 once, after
Init, also gave 55 of 55.
cv::Mat: RGB order and the real step
cv::Mat img = cv::imread("scan.jpg"); // BGR
cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
api.SetImage(img.data, img.cols, img.rows, img.channels(), img.step);
api.SetSourceResolution(300);
| Into SetImage | Command's text | Words |
|---|---|---|
| RGB, as above | 55 of 55 | 13,858 |
| BGR, as read | 16 of 55 | 13,854 |
| Gray, as read | 17 of 55 | 13,861 |
A crop shares rows with the full image. Pass
img.cols as bytes per line, and 10 cropped scans gave 0
words, with no error.
Init once, one API per thread
| Way | CPU |
|---|---|
| The command per line | 48.5 s |
Init per line |
35.5 s |
One Init, as above |
12.4 s |
Give each thread its own TessBaseAPI. Four threads read
the 55 scans in 34.6 s, not 102.2 s, with the same text.
PDFs: render pages with poppler-cpp
Error in pixReadStream: Pdf reading is not supported
Render each page in memory, gray, at 300 dpi. Add
poppler-cpp to the pkg-config line:
auto doc = std::unique_ptr<poppler::document>(
poppler::document::load_from_file("scan.pdf"));
poppler::page_renderer r;
r.set_image_format(poppler::image::format_gray8);
for (int i = 0; i < doc->pages(); i++) {
std::unique_ptr<poppler::page> page(doc->create_page(i));
poppler::image img = r.render_page(page.get(), 300, 300);
api.SetImage((const unsigned char *)img.const_data(), img.width(),
img.height(), 1, img.bytes_per_row());
api.SetSourceResolution(300);
char *text = api.GetUTF8Text();
printf("%s", text);
delete[] text;
}
1,882 of 1,955 words on 10 pages
Same bytes as pdftoppm -gray files
Debian in Docker: same text as apt's command
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ pkg-config libtesseract-dev tesseract-ocr-eng \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY main.cpp .
RUN g++ -std=c++17 -O2 main.cpp -o ocr $(pkg-config --cflags --libs tesseract lept)
ENTRYPOINT ["/app/ocr"]
With the code above as main.cpp, it matched apt's
tesseract (5.5.0) on 5 of 5 scans.
How I tested it
- Pages: 55 proofread Wikisource scans from 11 printed books, color JPEG, 1,920 pixels wide, no dpi in the file. The same set as Tesseract in PHP.
- Two columns: the 30 encyclopedia scans of the two-column test, letters found in order.
- Score: true words found in order, with the scorer of the tessdata test.
- Apple Silicon Mac, Apple clang 21, Homebrew Tesseract 5.5.4, Leptonica 1.87.0, OpenCV 5.0.0, Poppler 26.03.0, tessdata_fast English.
- CPU time of the whole process, on the Mac's efficiency cores. The command's text is compared with its last form feed dropped.
- PDF: the first 10 scans in one PDF, JPEG bytes kept. The JPEG files themselves gave 1,878 words.
What I did not test
- Windows, Visual Studio, vcpkg, Intel Macs, x86-64 Linux.
- The C API (capi.h), Qt images, and the PDF renderer.
- Other Tesseract versions, 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 C++ API give the same text as the command?
Yes, on 55 of 55 scans, with page mode 3 and 300 dpi set. With the API's defaults, 9 of 55 matched.
Why does the linker say pixRead is undefined?
pixRead is in Leptonica, and pkg-config tesseract does not list it. Add lept: pkg-config --cflags --libs tesseract lept.
Can the API read a PDF?
No. Leptonica reads images only. Render each page to an image first, for example with poppler-cpp at 300 dpi in gray, then pass the pixels to SetImage.