Commit fb87a84b for tesseract

commit fb87a84b6e3384b424b2169c76de9031046861e9
Author: İlkecan Bozdoğan <ilkecan@protonmail.com>
Date:   Wed Sep 2 11:50:46 2026 +0300

    Avoid redundant result iterator restarts (#4601)

    Avoid redundant paragraph and text-line restarts when a ResultIterator is already at the required physical start position.

    The reading-order setup repeatedly copied iterators and restarted them even when PageIterator::Next() had just placed them at a paragraph or text-line start. On sparse layouts with many blocks, those no-op restarts become costly repeated scans.

    -  guards paragraph and row restarts with the physical PageIterator::IsAtBeginningOf() predicate
    -  uses the same physical predicate to detect paragraph transitions after PageIterator::Next()
    -  adds focused LTR and RTL coverage for block, paragraph, and text-line iteration, including logical versus physical starts and an interior word

    Related to #4430.

    Reviewed-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/ccmain/resultiterator.cpp b/src/ccmain/resultiterator.cpp
index c3527b8b..d179666b 100644
--- a/src/ccmain/resultiterator.cpp
+++ b/src/ccmain/resultiterator.cpp
@@ -61,7 +61,9 @@ bool ResultIterator::CurrentParagraphIsLtr() const {
     return true; // doesn't matter.
   }
   LTRResultIterator it(*this);
-  it.RestartParagraph();
+  if (!it.PageIterator::IsAtBeginningOf(RIL_PARA)) {
+    it.RestartParagraph();
+  }
   // Try to figure out the ltr-ness of the paragraph.  The rules below
   // make more sense in the context of a difficult paragraph example.
   // Here we denote {ltr characters, RTL CHARACTERS}:
@@ -269,7 +271,9 @@ void ResultIterator::CalculateTextlineOrder(bool paragraph_is_ltr, const LTRResu

   // A LTRResultIterator goes strictly left-to-right word order.
   LTRResultIterator ltr_it(resit);
-  ltr_it.RestartRow();
+  if (!ltr_it.PageIterator::IsAtBeginningOf(RIL_TEXTLINE)) {
+    ltr_it.RestartRow();
+  }
   if (ltr_it.Empty(RIL_WORD)) {
     return;
   }
@@ -446,7 +450,9 @@ void ResultIterator::AppendSuffixMarks(std::string *text) const {

 void ResultIterator::MoveToLogicalStartOfTextline() {
   std::vector<int> word_indices;
-  RestartRow();
+  if (!PageIterator::IsAtBeginningOf(RIL_TEXTLINE)) {
+    RestartRow();
+  }
   CalculateTextlineOrder(current_paragraph_is_ltr_, dynamic_cast<const LTRResultIterator &>(*this),
                          &word_indices);
   unsigned i = 0;
@@ -489,7 +495,7 @@ bool ResultIterator::Next(PageIteratorLevel level) {
       if (!PageIterator::Next(level)) {
         return false;
       }
-      if (IsWithinFirstTextlineOfParagraph()) {
+      if (PageIterator::IsAtBeginningOf(RIL_PARA)) {
         // if we've advanced to a new paragraph,
         // recalculate current_paragraph_is_ltr_
         current_paragraph_is_ltr_ = CurrentParagraphIsLtr();
diff --git a/unittest/resultiterator_test.cc b/unittest/resultiterator_test.cc
index 12021081..e8d57ac0 100644
--- a/unittest/resultiterator_test.cc
+++ b/unittest/resultiterator_test.cc
@@ -32,9 +32,10 @@ protected:
   }
   ~ResultIteratorTest() override = default;

-  void SetImage(const char *filename) {
+  void SetImage(const char *filename, const char *language = "eng",
+                OcrEngineMode oem = tesseract::OEM_TESSERACT_ONLY) {
     src_pix_ = pixRead(TestDataNameToPath(filename).c_str());
-    api_.Init(TessdataPath().c_str(), "eng", tesseract::OEM_TESSERACT_ONLY);
+    api_.Init(TessdataPath().c_str(), language, oem);
     //    if (!FLAGS_tess_config.empty())
     //      api_.ReadConfigFile(FLAGS_tess_config.c_str());
     api_.SetPageSegMode(tesseract::PSM_AUTO);
@@ -402,6 +403,71 @@ TEST_F(ResultIteratorTest, GreyTest) {
   delete it;
 }

+// Tests that higher-level iteration lands at both the logical and physical
+// start of each LTR object, including when constructed from an interior word.
+TEST_F(ResultIteratorTest, IteratorLevelStartsTest) {
+  SetImage("8087_054.3B.tif");
+  char *text = api_.GetUTF8Text();
+  delete[] text;
+
+  const PageIteratorLevel levels[] = {RIL_BLOCK, RIL_PARA, RIL_TEXTLINE};
+  for (const auto level : levels) {
+    ResultIterator *it = api_.GetIterator();
+    ASSERT_NE(nullptr, it);
+    int count = 0;
+    do {
+      EXPECT_TRUE(it->IsAtBeginningOf(level));
+      EXPECT_TRUE(it->PageIterator::IsAtBeginningOf(level));
+      ++count;
+    } while (it->Next(level));
+    EXPECT_GT(count, 1);
+    delete it;
+  }
+
+  ResultIterator *it = api_.GetIterator();
+  ASSERT_NE(nullptr, it);
+  while (it->PageIterator::IsAtBeginningOf(RIL_TEXTLINE)) {
+    ASSERT_TRUE(it->Next(RIL_WORD));
+  }
+  EXPECT_FALSE(it->IsAtBeginningOf(RIL_TEXTLINE));
+  EXPECT_FALSE(it->PageIterator::IsAtBeginningOf(RIL_TEXTLINE));
+
+  ResultIterator *paragraph_start = ResultIterator::StartOfParagraph(*it);
+  ASSERT_NE(nullptr, paragraph_start);
+  EXPECT_TRUE(paragraph_start->IsAtBeginningOf(RIL_PARA));
+  EXPECT_TRUE(paragraph_start->PageIterator::IsAtBeginningOf(RIL_PARA));
+  delete paragraph_start;
+  delete it;
+}
+
+// Tests that RTL iteration remains at logical starts even when those differ
+// from the physical left-to-right starts used by PageIterator.
+TEST_F(ResultIteratorTest, RightToLeftIteratorStartsTest) {
+  SetImage("hebrew.png", "heb", tesseract::OEM_DEFAULT);
+  char *text = api_.GetUTF8Text();
+  ASSERT_NE(nullptr, text);
+  std::string truth(text);
+  delete[] text;
+
+  ResultIterator *it = api_.GetIterator();
+  ASSERT_NE(nullptr, it);
+  ASSERT_FALSE(it->ParagraphIsLtr());
+  int line_count = 0;
+  int logical_only_starts = 0;
+  do {
+    EXPECT_TRUE(it->IsAtBeginningOf(RIL_TEXTLINE));
+    if (!it->PageIterator::IsAtBeginningOf(RIL_TEXTLINE)) {
+      ++logical_only_starts;
+    }
+    ++line_count;
+  } while (it->Next(RIL_TEXTLINE));
+  EXPECT_GT(line_count, 1);
+  EXPECT_GT(logical_only_starts, 0);
+
+  VerifyAllText(truth, it);
+  delete it;
+}
+
 // Tests that Tesseract gets smallcaps and dropcaps.
 TEST_F(ResultIteratorTest, SmallCapDropCapTest) {
 #ifdef DISABLED_LEGACY_ENGINE