Commit 2a9cb0dc for tesseract
commit 2a9cb0dca6aaf89d26c2507ab69af079ec49ab9e
Author: Mohammad Sohail Shaikh <122166523+sohailshk@users.noreply.github.com>
Date: Sun Nov 15 02:38:02 2026 +0530
Fix crash in Recognize when no language was loaded (#4681)
Init(datapath, nullptr) loads no language data since f5d22d0b, but the
documentation still said nullptr defaults to eng. A failed Init whose
result is not checked leaves the same state. Recognize() then crashed in
Tesseract::recog_all_words.
Return -1 with an error message when no language data is loaded, and
correct the Init documentation.
Fixes #4028
diff --git a/include/tesseract/baseapi.h b/include/tesseract/baseapi.h
index 67a7f532..375c3907 100644
--- a/include/tesseract/baseapi.h
+++ b/include/tesseract/baseapi.h
@@ -169,8 +169,10 @@ public:
* listed above here in the class definition.
*
* The datapath must be the name of the tessdata directory.
- * The language is (usually) an ISO 639-3 string or nullptr will default to
- * eng. It is entirely safe (and eventually will be efficient too) to call
+ * The language is (usually) an ISO 639-3 string. If it is nullptr or empty,
+ * no language data is loaded: this is enough for page layout analysis, but
+ * Recognize fails (returns -1) until Init is called with a language.
+ * It is entirely safe (and eventually will be efficient too) to call
* Init multiple times on the same instance to change language, or just
* to reset the classifier.
* The language may be a string of the form [~]<lang>[+[~]<lang>]* indicating
diff --git a/src/api/baseapi.cpp b/src/api/baseapi.cpp
index 45d58867..676200e0 100644
--- a/src/api/baseapi.cpp
+++ b/src/api/baseapi.cpp
@@ -292,7 +292,8 @@ void TessBaseAPI::PrintVariables(FILE *fp) const {
/**
* The datapath must be the name of the data directory or
* some other file in which the data directory resides (for instance argv[0].)
- * The language is (usually) an ISO 639-3 string or nullptr will default to eng.
+ * The language is (usually) an ISO 639-3 string. If it is nullptr or empty,
+ * no language data is loaded and Recognize fails.
* If numeric_mode is true, then only digits and Roman numerals will
* be returned.
* @return: 0 on success and -1 on initialization failure.
@@ -321,8 +322,8 @@ int TessBaseAPI::Init(const char *data, int data_size, const char *language, Ocr
// If the datapath, OcrEngineMode or the language have changed - start again.
// Note that the language_ field stores the last requested language that was
// initialized successfully, while tesseract_->lang stores the language
- // actually used. They differ only if the requested language was nullptr, in
- // which case tesseract_->lang is set to the Tesseract default ("eng").
+ // actually used. If the requested language was nullptr or empty, no language
+ // is loaded and tesseract_->lang stays empty.
if (tesseract_ != nullptr &&
(datapath_.empty() || language_.empty() || datapath_ != datapath ||
last_oem_requested_ != oem || (language_ != language && tesseract_->lang != language))) {
@@ -765,6 +766,19 @@ int TessBaseAPI::Recognize(ETEXT_DESC *monitor) {
if (tesseract_ == nullptr) {
return -1;
}
+ // Training line recognition (tessedit_train_line_recognizer, used by the
+ // lstm.train config together with tessedit_init_config_only) needs no model.
+ if (!tesseract_->tessedit_train_line_recognizer &&
+ tesseract_->unicharset.size() <= SPECIAL_UNICHAR_CODES_COUNT) {
+ // Only the special unichars are present, so no language data was loaded:
+ // Init was called without a language (which loads nothing), Init failed
+ // before the unicharset was loaded (for example because the traineddata
+ // file is missing), or only InitForAnalysePage was used. Recognition needs
+ // a model. An Init that fails after the unicharset was loaded is not
+ // detected here.
+ tprintf("Error: Recognize called without a loaded language. Pass a language to Init.\n");
+ return -1;
+ }
if (FindLines() != 0) {
return -1;
}
diff --git a/unittest/baseapi_test.cc b/unittest/baseapi_test.cc
index 88a33e54..dbfc5b8c 100644
--- a/unittest/baseapi_test.cc
+++ b/unittest/baseapi_test.cc
@@ -23,6 +23,8 @@
#include "gmock/gmock-matchers.h"
+#include <cstdio>
+#include <filesystem>
#include <memory>
#include <regex>
#include <sstream>
@@ -515,6 +517,65 @@ TEST_F(TesseractTest, InitConfigOnlyTest) {
}
}
+// Tests that recognition fails cleanly (instead of crashing) when no language
+// data is loaded. Init without a language loads nothing, and the result of a
+// failed Init is often not checked.
+TEST_F(TesseractTest, RecognizeWithoutLanguage) {
+ // A white page with a row of hollow black boxes, so that layout analysis
+ // finds a text line to recognize.
+ Image text_pix = pixCreate(600, 120, 8);
+ CHECK(text_pix);
+ pixSetAll(text_pix);
+ for (int x = 20; x < 560; x += 36) {
+ Box *box = boxCreate(x, 30, 22, 44);
+ pixRenderBox(text_pix, box, 4, L_CLEAR_PIXELS);
+ boxDestroy(&box);
+ }
+ {
+ tesseract::TessBaseAPI api;
+ EXPECT_EQ(0, api.Init(TessdataPath().c_str(), nullptr));
+ api.SetImage(text_pix);
+ EXPECT_EQ(-1, api.Recognize(nullptr));
+ EXPECT_EQ(nullptr, api.GetUTF8Text());
+ }
+ {
+ tesseract::TessBaseAPI api;
+ EXPECT_EQ(-1, api.Init(TessdataPath().c_str(), "no_such_language"));
+ api.SetImage(text_pix);
+ EXPECT_EQ(-1, api.Recognize(nullptr));
+ EXPECT_EQ(nullptr, api.GetUTF8Text());
+ }
+ text_pix.destroy();
+}
+
+// Tests that creating training data (the lstm.train config) still works: it
+// sets tessedit_init_config_only, so no unicharset is loaded, but needs no
+// model to turn the line boxes into an .lstmf file.
+TEST_F(TesseractTest, TrainLineRecognizerWithoutModel) {
+ REQUIRE_TRAINEDDATA(TESSDATA_DIR, "eng");
+ const std::string image_file = TestDataNameToPath("trainingtimesline.tif");
+ const std::string output_base = file::JoinPath(FLAGS_test_tmpdir, "trainingtimesline");
+ const std::string lstmf_file = output_base + ".lstmf";
+ file::MakeTmpdir();
+ std::remove(lstmf_file.c_str());
+ Image pix = pixRead(image_file.c_str());
+ ASSERT_TRUE(pix != nullptr);
+ std::vector<std::string> vars_vec = {"tessedit_init_config_only",
+ "tessedit_train_line_recognizer"};
+ std::vector<std::string> vars_values = {"1", "1"};
+ tesseract::TessBaseAPI api;
+ ASSERT_EQ(0, api.Init(TessdataPath().c_str(), "eng", tesseract::OEM_DEFAULT, nullptr, 0,
+ &vars_vec, &vars_values, false));
+ api.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK);
+ api.SetImage(pix);
+ api.SetInputName(image_file.c_str());
+ api.SetOutputName(output_base.c_str());
+ EXPECT_EQ(0, api.Recognize(nullptr));
+ EXPECT_TRUE(std::filesystem::exists(lstmf_file));
+ std::remove(lstmf_file.c_str());
+ pix.destroy();
+}
+
// Tests if two instances of Tesseract/LSTM can co-exist in the same thread.
// NOTE: This is not an exhaustive test and current support for multiple
// instances in Tesseract is fragile. This test is intended largely as a means