Commit 5a91677d for tesseract

commit 5a91677d11b870b5eac021c7178feebe1c9f617c
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Nov 13 17:39:02 2026 +0100

    api: run the recognition if only the layout analysis was done

    GetHOCRText(), GetAltoText(), GetPAGEText(), GetTSVText(),
    GetLSTMBoxText() and GetWordStrBoxText() only called Recognize() if
    page_res_ was null. After AnalyseLayout(), which is all that
    ProcessPage() does for tessedit_pageseg_mode 2 (PSM_AUTO_ONLY),
    page_res_ exists without recognition results, and the iterator
    dereferenced a null pointer.

    Test recognition_done_ like GetUTF8Text(), GetBoxText() and
    GetUNLVText() do.

    The PDF renderer uses the iterator directly and crashed in the same
    way, so it gets the same test. It needs to be a friend class of
    TessBaseAPI for that.

    This change needs the fix for the state of recognition_done_ on empty
    pages first (pull request #4528). Without that fix Recognize() does
    not set recognition_done_ for an empty page, so each of these
    functions would run the layout analysis of an empty page again.

    Fix proposed by the Anthropic OSS Scanner (finding ANT-2026-RBQXRM52).

    Assisted-by: OpenCode / Claude Fable 5.2 (Anthropic)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/include/tesseract/baseapi.h b/include/tesseract/baseapi.h
index 375c3907..1e0670d4 100644
--- a/include/tesseract/baseapi.h
+++ b/include/tesseract/baseapi.h
@@ -53,6 +53,7 @@ class ImageThresholder;
 class LTRResultIterator;
 class ResultIterator;
 class MutableIterator;
+class TessPDFRenderer;
 class TessResultRenderer;
 class Tesseract;

@@ -74,6 +75,8 @@ using ProbabilityInContextFunc = double (Dict::*)(const char *, const char *,
  * include any other Tesseract headers.
  */
 class TESS_API TessBaseAPI {
+  friend class TessPDFRenderer;
+
 public:
   TessBaseAPI();
   virtual ~TessBaseAPI();
diff --git a/src/api/altorenderer.cpp b/src/api/altorenderer.cpp
index 7225ddba..8e72aca2 100644
--- a/src/api/altorenderer.cpp
+++ b/src/api/altorenderer.cpp
@@ -154,7 +154,8 @@ char *TessBaseAPI::GetAltoText(int page_number) {
 /// data structures.
 ///
 char *TessBaseAPI::GetAltoText(ETEXT_DESC *monitor, int page_number) {
-  if (tesseract_ == nullptr || (page_res_ == nullptr && Recognize(monitor) < 0)) {
+  if (tesseract_ == nullptr ||
+      ((page_res_ == nullptr || !recognition_done_) && Recognize(monitor) < 0)) {
     return nullptr;
   }

diff --git a/src/api/baseapi.cpp b/src/api/baseapi.cpp
index 676200e0..772d02f5 100644
--- a/src/api/baseapi.cpp
+++ b/src/api/baseapi.cpp
@@ -1367,7 +1367,8 @@ static void AddBoxToTSV(const PageIterator *it, PageIteratorLevel level, std::st
  * Returned string must be freed with the delete [] operator.
  */
 char *TessBaseAPI::GetTSVText(int page_number) {
-  if (tesseract_ == nullptr || (page_res_ == nullptr && Recognize(nullptr) < 0)) {
+  if (tesseract_ == nullptr ||
+      ((page_res_ == nullptr || !recognition_done_) && Recognize(nullptr) < 0)) {
     return nullptr;
   }

diff --git a/src/api/hocrrenderer.cpp b/src/api/hocrrenderer.cpp
index 5f319eca..32da90e4 100644
--- a/src/api/hocrrenderer.cpp
+++ b/src/api/hocrrenderer.cpp
@@ -131,7 +131,8 @@ char *TessBaseAPI::GetHOCRText(int page_number) {
  */
 char *TessBaseAPI::GetHOCRText(ETEXT_DESC *monitor, int page_number) {
   if (tesseract_ == nullptr ||
-      (page_res_ == nullptr && Recognize(monitor) < 0)) {
+      ((page_res_ == nullptr || !recognition_done_) &&
+       Recognize(monitor) < 0)) {
     return nullptr;
   }

diff --git a/src/api/lstmboxrenderer.cpp b/src/api/lstmboxrenderer.cpp
index 1514eeed..43c269c9 100644
--- a/src/api/lstmboxrenderer.cpp
+++ b/src/api/lstmboxrenderer.cpp
@@ -37,7 +37,8 @@ static void AddBoxToLSTM(int right, int bottom, int top, int image_height, int p
 }

 char *TessBaseAPI::GetLSTMBoxText(int page_number = 0) {
-  if (tesseract_ == nullptr || (page_res_ == nullptr && Recognize(nullptr) < 0)) {
+  if (tesseract_ == nullptr ||
+      ((page_res_ == nullptr || !recognition_done_) && Recognize(nullptr) < 0)) {
     return nullptr;
   }

diff --git a/src/api/pagerenderer.cpp b/src/api/pagerenderer.cpp
index 6108045a..5ffa0296 100644
--- a/src/api/pagerenderer.cpp
+++ b/src/api/pagerenderer.cpp
@@ -712,7 +712,8 @@ char *TessBaseAPI::GetPAGEText(int page_number) {
 ///
 char *TessBaseAPI::GetPAGEText(ETEXT_DESC *monitor, int /*page_number*/) {
   if (tesseract_ == nullptr ||
-      (page_res_ == nullptr && Recognize(monitor) < 0)) {
+      ((page_res_ == nullptr || !recognition_done_) &&
+       Recognize(monitor) < 0)) {
     return nullptr;
   }

diff --git a/src/api/pdfrenderer.cpp b/src/api/pdfrenderer.cpp
index 47f35185..5173b75b 100644
--- a/src/api/pdfrenderer.cpp
+++ b/src/api/pdfrenderer.cpp
@@ -825,6 +825,12 @@ bool TessPDFRenderer::imageToPDFObj(Pix *pix, const char *filename, long int obj
 }

 bool TessPDFRenderer::AddImageHandler(TessBaseAPI *api) {
+  // The text layer needs recognition results, but page_res_ may only hold
+  // layout analysis results here (ProcessPage skips recognition for
+  // PSM_AUTO_ONLY and PSM_OSD_ONLY), so run it like the Get*Text functions do.
+  if ((api->page_res_ == nullptr || !api->recognition_done_) && api->Recognize(nullptr) < 0) {
+    return false;
+  }
   Pix *pix = api->GetInputImage();
   const char *filename = api->GetInputName();
   int ppi = api->GetSourceYResolution();
diff --git a/src/api/wordstrboxrenderer.cpp b/src/api/wordstrboxrenderer.cpp
index e4ffcedb..c9baf00a 100644
--- a/src/api/wordstrboxrenderer.cpp
+++ b/src/api/wordstrboxrenderer.cpp
@@ -30,7 +30,8 @@ namespace tesseract {
  */

 char *TessBaseAPI::GetWordStrBoxText(int page_number = 0) {
-  if (tesseract_ == nullptr || (page_res_ == nullptr && Recognize(nullptr) < 0)) {
+  if (tesseract_ == nullptr ||
+      ((page_res_ == nullptr || !recognition_done_) && Recognize(nullptr) < 0)) {
     return nullptr;
   }