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