Commit 5a575454 for tesseract

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

    Clip the SetRectangle rectangle to the image (#4683)

    ImageThresholder::SetRectangle stored the rectangle unchecked, but
    ThresholdRectToPix and OtsuThreshold read rect_width_ x rect_height_
    pixels from the source image. A rectangle reaching past the right or
    bottom edge (or starting at a negative coordinate) read outside of the
    image buffer; AddressSanitizer reports a heap-buffer-overflow.

    Clip the rectangle to the image in SetRectangle, and make Threshold fail
    cleanly for an empty rectangle, which crashed in OtsuThreshold.

diff --git a/include/tesseract/baseapi.h b/include/tesseract/baseapi.h
index 06b17360..67a7f532 100644
--- a/include/tesseract/baseapi.h
+++ b/include/tesseract/baseapi.h
@@ -325,6 +325,7 @@ public:

   /**
    * Restrict recognition to a sub-rectangle of the image. Call after SetImage.
+   * The rectangle is clipped to the image.
    * Each SetRectangle clears the recogntion results so multiple rectangles
    * can be recognized with the same image.
    */
diff --git a/src/api/baseapi.cpp b/src/api/baseapi.cpp
index 5fb9f9e5..45d58867 100644
--- a/src/api/baseapi.cpp
+++ b/src/api/baseapi.cpp
@@ -541,6 +541,7 @@ void TessBaseAPI::SetImage(Pix *pix) {

 /**
  * Restrict recognition to a sub-rectangle of the image. Call after SetImage.
+ * The rectangle is clipped to the image.
  * Each SetRectangle clears the recognition results so multiple rectangles
  * can be recognized with the same image.
  */
@@ -1998,6 +1999,12 @@ bool TessBaseAPI::Threshold(Pix **pix) {
   if (*pix != nullptr) {
     pixDestroy(pix);
   }
+  thresholder_->GetImageSizes(&rect_left_, &rect_top_, &rect_width_, &rect_height_, &image_width_,
+                              &image_height_);
+  if (rect_width_ <= 0 || rect_height_ <= 0) {
+    tprintf("Error: The rectangle to recognize is empty or outside of the image.\n");
+    return false;
+  }
   // Zero resolution messes up the algorithms, so make sure it is credible.
   int user_dpi = 0;
   GetIntVariable("user_defined_dpi", &user_dpi);
@@ -2048,9 +2055,6 @@ bool TessBaseAPI::Threshold(Pix **pix) {
     tesseract_->set_pix_grey(pix_grey);
   }

-  thresholder_->GetImageSizes(&rect_left_, &rect_top_, &rect_width_, &rect_height_, &image_width_,
-                              &image_height_);
-
   // Set the internal resolution that is used for layout parameters from the
   // estimated resolution, rather than the image resolution, which may be
   // fabricated, but we will use the image resolution, if there is one, to
diff --git a/src/ccmain/thresholder.cpp b/src/ccmain/thresholder.cpp
index 8d92c352..4fb2af35 100644
--- a/src/ccmain/thresholder.cpp
+++ b/src/ccmain/thresholder.cpp
@@ -28,7 +28,7 @@

 #include <tesseract/baseapi.h> // for api->GetIntVariable()

-#include <algorithm> // for std::max, std::min
+#include <algorithm> // for std::clamp, std::max, std::min
 #include <cstdint>   // for uint32_t
 #include <cstring>
 #include <tuple>
@@ -130,11 +130,15 @@ void ImageThresholder::SetImage(const unsigned char *imagedata, int width, int h

 // Store the coordinates of the rectangle to process for later use.
 // Doesn't actually do any thresholding.
+// The rectangle is clipped to the image, as the thresholding code reads
+// rect_width_ x rect_height_ pixels from the source image without checking.
 void ImageThresholder::SetRectangle(int left, int top, int width, int height) {
-  rect_left_ = left;
-  rect_top_ = top;
-  rect_width_ = width;
-  rect_height_ = height;
+  const int64_t right = std::min<int64_t>(static_cast<int64_t>(left) + width, image_width_);
+  const int64_t bottom = std::min<int64_t>(static_cast<int64_t>(top) + height, image_height_);
+  rect_left_ = std::clamp(left, 0, image_width_);
+  rect_top_ = std::clamp(top, 0, image_height_);
+  rect_width_ = static_cast<int>(std::max<int64_t>(right - rect_left_, 0));
+  rect_height_ = static_cast<int>(std::max<int64_t>(bottom - rect_top_, 0));
 }

 // Get enough parameters to be able to rebuild bounding boxes in the
diff --git a/src/ccmain/thresholder.h b/src/ccmain/thresholder.h
index e20c065b..908695c0 100644
--- a/src/ccmain/thresholder.h
+++ b/src/ccmain/thresholder.h
@@ -65,6 +65,7 @@ public:
                 int bytes_per_line);

   /// Store the coordinates of the rectangle to process for later use.
+  /// The rectangle is clipped to the image, so it may end up empty.
   /// Doesn't actually do any thresholding.
   void SetRectangle(int left, int top, int width, int height);

diff --git a/unittest/baseapi_test.cc b/unittest/baseapi_test.cc
index fa886e01..88a33e54 100644
--- a/unittest/baseapi_test.cc
+++ b/unittest/baseapi_test.cc
@@ -290,6 +290,51 @@ TEST_F(TesseractTest, AdaptToWordStrTest) {
 #endif
 }

+// A rectangle that extends beyond the image must be clipped to the image.
+// Thresholding reads rect_width_ x rect_height_ pixels from the source image
+// and used to read past its end.
+TEST_F(TesseractTest, SetRectangleIsClippedToImage) {
+  Image src_pix = pixCreate(100, 60, 8);
+  CHECK(src_pix);
+  pixSetAll(src_pix);
+  Box *box = boxCreate(10, 10, 30, 20);
+  pixClearInRect(src_pix, box);
+  boxDestroy(&box);
+  struct TestCase {
+    int left, top, width, height;
+    int expected_width, expected_height;
+  };
+  const TestCase cases[] = {
+      {10, 10, 20, 20, 20, 20},         // Inside the image: unchanged.
+      {80, 40, 50, 50, 20, 20},         // Beyond the right and bottom edges.
+      {-10, -10, 30, 30, 20, 20},       // Beyond the left and top edges.
+      {90, 0, 1000000000, 60, 10, 60},  // Huge width must not overflow.
+  };
+  for (const auto &test : cases) {
+    tesseract::TessBaseAPI api;
+    api.InitForAnalysePage();
+    api.SetImage(src_pix);
+    api.SetRectangle(test.left, test.top, test.width, test.height);
+    Pix *thresholded = api.GetThresholdedImage();
+    ASSERT_NE(nullptr, thresholded);
+    EXPECT_EQ(test.expected_width, pixGetWidth(thresholded));
+    EXPECT_EQ(test.expected_height, pixGetHeight(thresholded));
+    pixDestroy(&thresholded);
+  }
+  // An empty rectangle, or one that is entirely outside of the image, cannot
+  // be recognized: this must fail cleanly.
+  const int empty_rects[][4] = {
+      {0, 0, 0, 0}, {10, 10, 0, 20}, {200, 200, 10, 10}, {-50, 0, 20, 20}};
+  for (const auto &rect : empty_rects) {
+    tesseract::TessBaseAPI api;
+    api.InitForAnalysePage();
+    api.SetImage(src_pix);
+    api.SetRectangle(rect[0], rect[1], rect[2], rect[3]);
+    EXPECT_EQ(nullptr, api.GetThresholdedImage());
+  }
+  src_pix.destroy();
+}
+
 // Tests that LSTM gets exactly the right answer on phototest.
 TEST_F(TesseractTest, BasicLSTMTest) {
   tesseract::TessBaseAPI api;