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;