Commit 565e3bd7 for tesseract
commit 565e3bd72a0ecc4814ccffbda3f17afcb40d75ef
Author: Aryan R. Ganjoo <51299383+AryanRG@users.noreply.github.com>
Date: Mon Oct 19 01:20:19 2026 +0530
Escape input names in ALTO and PAGE XML (#4652)
Escape XML-special characters when serializing ALTO and PAGE input names, while preserving the raw PAGE name for reading-order identity. Add focused regressions for ALTO fileName and PAGE imageFilename/externalRef output.
Assisted-by: Codex and Claude Opus 5.5
diff --git a/src/api/altorenderer.cpp b/src/api/altorenderer.cpp
index f207c1d2..7225ddba 100644
--- a/src/api/altorenderer.cpp
+++ b/src/api/altorenderer.cpp
@@ -95,7 +95,10 @@ bool TessAltoRenderer::AddImageHandler(TessBaseAPI *api) {
"\t\t<MeasurementUnit>pixel</MeasurementUnit>\n"
"\t\t<sourceImageInformation>\n"
"\t\t\t<fileName>");
- AppendString(api->GetInputName());
+ const char *input_name = api->GetInputName();
+ if (input_name != nullptr) {
+ AppendString(HOcrEscape(input_name).c_str());
+ }
AppendString(
"</fileName>\n"
"\t\t</sourceImageInformation>\n"
diff --git a/src/api/pagerenderer.cpp b/src/api/pagerenderer.cpp
index 591edefe..41f5f550 100644
--- a/src/api/pagerenderer.cpp
+++ b/src/api/pagerenderer.cpp
@@ -641,10 +641,11 @@ bool TessPAGERenderer::AddImageHandler(TessBaseAPI *api) {
// If a URL is used to recognize an image add it as <Metadata
// externalRef="url">
- if (std::regex_search(api->GetInputName(),
- std::regex("^(https?|ftp|ssh):"))) {
+ const char *input_name = api->GetInputName();
+ if (input_name != nullptr &&
+ std::regex_search(input_name, std::regex("^(https?|ftp|ssh):"))) {
AppendString(" externalRef=\"");
- AppendString(api->GetInputName());
+ AppendString(HOcrEscape(input_name).c_str());
AppendString("\" ");
}
@@ -762,11 +763,14 @@ char *TessBaseAPI::GetPAGEText(ETEXT_DESC *monitor, int /*page_number*/) {
// Use "C" locale (needed for int values larger than 999).
page_str.imbue(std::locale::classic());
- reading_order_str << "\t<Page " << "imageFilename=\"" << GetInputName();
- // AppendString(api->GetInputName());
+ reading_order_str << "\t<Page " << "imageFilename=\"";
+ const char *input_name = GetInputName();
+ if (input_name != nullptr) {
+ reading_order_str << HOcrEscape(input_name);
+ }
reading_order_str << "\" " << "imageWidth=\"" << rect_width_ << "\" "
<< "imageHeight=\"" << rect_height_ << "\">\n";
- std::size_t ro_id = std::hash<std::string>{}(GetInputName());
+ std::size_t ro_id = std::hash<std::string>{}(input_name ? input_name : "");
reading_order_str << "\t\t<ReadingOrder>\n"
<< "\t\t\t<OrderedGroup id=\"ro" << ro_id
<< "\" caption=\"Regions reading order\">\n";
diff --git a/unittest/baseapi_test.cc b/unittest/baseapi_test.cc
index 9336b5ad..d8985d25 100644
--- a/unittest/baseapi_test.cc
+++ b/unittest/baseapi_test.cc
@@ -18,6 +18,7 @@
#include "pageres.h"
#include <tesseract/baseapi.h>
+#include <tesseract/renderer.h>
#include "gmock/gmock-matchers.h"
@@ -471,4 +472,80 @@ TEST_F(TesseractTest, PAGEXMLMultiPageClosingTags) {
src_pix.destroy();
}
+TEST_F(TesseractTest, PAGEEscapesImageFilename) {
+ tesseract::TessBaseAPI api;
+ if (api.Init(TessdataPath().c_str(), "eng", tesseract::OEM_LSTM_ONLY) == -1) {
+ GTEST_SKIP();
+ }
+
+ Image src_pix = pixRead(TestDataNameToPath("HelloGoogle.tif").c_str());
+ ASSERT_TRUE(src_pix);
+ api.SetInputName("alpha&beta.pgm");
+ api.SetImage(src_pix);
+
+ std::unique_ptr<const char[]> page(api.GetPAGEText(0));
+ ASSERT_TRUE(page != nullptr);
+ EXPECT_THAT(page.get(), HasSubstr("imageFilename=\"alpha&beta.pgm\""));
+
+ src_pix.destroy();
+}
+
+TEST_F(TesseractTest, ALTOEscapesFileName) {
+ file::MakeTmpdir();
+ const std::string output_base = file::JoinPath(FLAGS_test_tmpdir, "alto-escaped-input-name");
+ tesseract::TessBaseAPI api;
+ if (api.Init(TessdataPath().c_str(), "eng", tesseract::OEM_LSTM_ONLY) == -1) {
+ GTEST_SKIP();
+ }
+
+ Image src_pix = pixRead(TestDataNameToPath("HelloGoogle.tif").c_str());
+ ASSERT_TRUE(src_pix);
+ api.SetInputName("alpha&beta.pgm");
+ api.SetImage(src_pix);
+
+ {
+ tesseract::TessAltoRenderer renderer(output_base.c_str());
+ ASSERT_TRUE(renderer.BeginDocument("escaped input name"));
+ ASSERT_TRUE(renderer.AddImage(&api));
+ ASSERT_TRUE(renderer.EndDocument());
+ }
+
+ std::string alto;
+ CHECK_OK(file::GetContents(output_base + ".xml", &alto, file::Defaults()));
+ EXPECT_THAT(alto, HasSubstr("<fileName>alpha&beta.pgm</fileName>"));
+
+ src_pix.destroy();
+}
+
+TEST_F(TesseractTest, PAGERendererEscapesExternalRefAndImageFilename) {
+ file::MakeTmpdir();
+ const std::string output_base = file::JoinPath(FLAGS_test_tmpdir, "page-escaped-input-name");
+ constexpr const char *kInputName = "https://example.test/image?a=1&b=2";
+ tesseract::TessBaseAPI api;
+ if (api.Init(TessdataPath().c_str(), "eng", tesseract::OEM_LSTM_ONLY) == -1) {
+ GTEST_SKIP();
+ }
+
+ Image src_pix = pixRead(TestDataNameToPath("HelloGoogle.tif").c_str());
+ ASSERT_TRUE(src_pix);
+ api.SetInputName(kInputName);
+ api.SetImage(src_pix);
+
+ {
+ tesseract::TessPAGERenderer renderer(output_base.c_str());
+ ASSERT_TRUE(renderer.BeginDocument("escaped input name"));
+ ASSERT_TRUE(renderer.AddImage(&api));
+ ASSERT_TRUE(renderer.EndDocument());
+ }
+
+ std::string page;
+ CHECK_OK(file::GetContents(output_base + ".page.xml", &page, file::Defaults()));
+ EXPECT_THAT(page,
+ HasSubstr("externalRef=\"https://example.test/image?a=1&b=2\""));
+ EXPECT_THAT(page,
+ HasSubstr("imageFilename=\"https://example.test/image?a=1&b=2\""));
+
+ src_pix.destroy();
+}
+
} // namespace tesseract