Commit c1ea70b9 for tesseract

commit c1ea70b9bd5c136c53213532dd902474e269561c
Author: Mohammad Sohail Shaikh <122166523+sohailshk@users.noreply.github.com>
Date:   Sat Nov 14 03:09:22 2026 +0530

    Make DetectOS fail without a legacy model instead of throwing (#4682)

    Orientation and script detection classifies blobs with the legacy
    classifier of the primary language. Without its templates (an LSTM only
    model such as tessdata_fast or tessdata_best eng, a primary language
    with a legacy-less model plus osd, or no language at all) the blob
    choices carry no script id, and ScriptDetector::detect_blob uses it as
    an index into the "done" vector. DetectOrientationScript then ended with
    an uncaught std::out_of_range, which a caller of the C API cannot catch,
    and aborted the program.

    Return false with an error message instead, document it, and add a
    unit test.

    Fixes #4062

diff --git a/include/tesseract/baseapi.h b/include/tesseract/baseapi.h
index 9475fb27..06b17360 100644
--- a/include/tesseract/baseapi.h
+++ b/include/tesseract/baseapi.h
@@ -613,7 +613,11 @@ public:
    * orient_conf is the confidence (15.0 is reasonably confident)
    * script_name is an ASCII string, the name of the script, e.g. "Latin"
    * script_conf is confidence level in the script
-   * Returns true on success and writes values to each parameter as an output
+   * Returns true on success and writes values to each parameter as an output.
+   * Returns false if the primary (first) language was loaded without the
+   * legacy engine (for example the tessdata_fast or tessdata_best model of a
+   * language, or a combined model loaded with OEM_LSTM_ONLY): use the osd
+   * model, or put a model with a legacy engine first in the language list.
    */
   bool DetectOrientationScript(int *orient_deg, float *orient_conf,
                                const char **script_name, float *script_conf);
diff --git a/src/api/baseapi.cpp b/src/api/baseapi.cpp
index 1331db5d..5fb9f9e5 100644
--- a/src/api/baseapi.cpp
+++ b/src/api/baseapi.cpp
@@ -2220,6 +2220,14 @@ bool TessBaseAPI::DetectOS(OSResults *osr) {
   if (tesseract_ == nullptr) {
     return false;
   }
+  if (tesseract_->PreTrainedTemplates == nullptr) {
+    // Orientation and script detection classifies with the legacy classifier
+    // of the primary language. Without its templates (for example the
+    // tessdata_fast or tessdata_best model of a language other than osd, or
+    // layout analysis only) the detector gets no script information.
+    tprintf("Error: OSD requires a model for the legacy engine, e.g. osd.traineddata\n");
+    return false;
+  }
   ClearResults();
   if (tesseract_->pix_binary() == nullptr && !Threshold(&tesseract_->mutable_pix_binary()->pix_)) {
     return false;
diff --git a/unittest/baseapi_test.cc b/unittest/baseapi_test.cc
index f10caa80..fa886e01 100644
--- a/unittest/baseapi_test.cc
+++ b/unittest/baseapi_test.cc
@@ -400,6 +400,46 @@ TEST_F(TesseractTest, OsdEngineUsesMinCharactersToTry) {
 #endif
 }

+// Orientation and script detection needs a model for the legacy engine of the
+// primary language. Without one it must fail instead of throwing an exception.
+TEST_F(TesseractTest, DetectOrientationScriptNeedsLegacyModel) {
+#ifdef DISABLED_LEGACY_ENGINE
+  // Skip test because TessBaseAPI::DetectOrientationScript is missing.
+  GTEST_SKIP();
+#else
+  REQUIRE_TRAINEDDATA(TESSDATA_DIR, "eng");
+  REQUIRE_TRAINEDDATA(TESSDATA_DIR, "osd");
+  Image src_pix = pixRead(TestDataNameToPath("phototest.tif").c_str());
+  CHECK(src_pix);
+  // Each case initializes the API with the given language, or skips Init.
+  for (int test_case = 0; test_case < 3; ++test_case) {
+    tesseract::TessBaseAPI api;
+    switch (test_case) {
+      case 0: // LSTM only model.
+        ASSERT_EQ(0, api.Init(TessdataPath().c_str(), "eng", tesseract::OEM_LSTM_ONLY));
+        break;
+      case 1: // The primary language has no legacy model, a secondary one has.
+        // osd has no LSTM model, so it falls back to the legacy engine, while
+        // eng is loaded without its legacy model even if the file has one.
+        ASSERT_EQ(0, api.Init(TessdataPath().c_str(), "eng+osd", tesseract::OEM_LSTM_ONLY));
+        break;
+      default: // Layout analysis only.
+        api.InitForAnalysePage();
+        break;
+    }
+    api.SetImage(src_pix);
+    int orient_deg = -1;
+    float orient_conf = 0;
+    const char *script_name = nullptr;
+    float script_conf = 0;
+    EXPECT_FALSE(
+        api.DetectOrientationScript(&orient_deg, &orient_conf, &script_name, &script_conf))
+        << "case " << test_case;
+  }
+  src_pix.destroy();
+#endif
+}
+
 TEST_F(TesseractTest, InitConfigOnlyTest) {
   // Languages for testing initialization.
   const char *langs[] = {"eng", "chi_tra", "jpn", "vie"};