#ifdef HAVE_CONFIG_H
# include "config_auto.h"
#endif
#include "tablerecog.h"
#include <algorithm>
namespace tesseract {
const double kHorizontalSpacing = 0.30;
const double kVerticalSpacing = -0.2;
const int kCellSplitRowThreshold = 0;
const int kCellSplitColumnThreshold = 0;
const int kLinedTableMinVerticalLines = 3;
const int kLinedTableMinHorizontalLines = 3;
const double kRequiredColumns = 0.7;
const double kMarginFactor = 1.1;
const double kMaxRowSize = 2.5;
const double kGoodRowNumberOfColumnsSmall[] = {2, 2, 2, 2, 2, 3, 3};
const double kGoodRowNumberOfColumnsLarge = 0.7;
const double kMinFilledArea = 0.35;
static bool IsWeakTableRow(StructuredTable *table, int row) {
if (!table->VerifyRowFilled(row)) {
return false;
}
double threshold;
if (table->column_count() < countof(kGoodRowNumberOfColumnsSmall)) {
threshold = kGoodRowNumberOfColumnsSmall[table->column_count()];
} else {
threshold = table->column_count() * kGoodRowNumberOfColumnsLarge;
}
return table->CountFilledCellsInRow(row) < threshold;
}
StructuredTable::StructuredTable()
: text_grid_(nullptr)
, line_grid_(nullptr)
, is_lined_(false)
, space_above_(0)
, space_below_(0)
, space_left_(0)
, space_right_(0)
, median_cell_height_(0)
, median_cell_width_(0)
, max_text_height_(INT32_MAX) {}
void StructuredTable::Init() {}
void StructuredTable::set_text_grid(ColPartitionGrid *text_grid) {
text_grid_ = text_grid;
}
void StructuredTable::set_line_grid(ColPartitionGrid *line_grid) {
line_grid_ = line_grid;
}
void StructuredTable::set_max_text_height(int height) {
max_text_height_ = height;
}
bool StructuredTable::is_lined() const {
return is_lined_;
}
unsigned StructuredTable::row_count() const {
return cell_y_.empty() ? 0 : cell_y_.size() - 1;
}
unsigned StructuredTable::column_count() const {
return cell_x_.empty() ? 0 : cell_x_.size() - 1;
}
unsigned StructuredTable::cell_count() const {
return row_count() * column_count();
}
void StructuredTable::set_bounding_box(const TBOX &box) {
bounding_box_ = box;
}
const TBOX &StructuredTable::bounding_box() const {
return bounding_box_;
}
int StructuredTable::median_cell_height() {
return median_cell_height_;
}
int StructuredTable::median_cell_width() {
return median_cell_width_;
}
int StructuredTable::row_height(unsigned row) const {
ASSERT_HOST(row < row_count());
return cell_y_[row + 1] - cell_y_[row];
}
int StructuredTable::column_width(unsigned column) const {
ASSERT_HOST(column < column_count());
return cell_x_[column + 1] - cell_x_[column];
}
int StructuredTable::space_above() const {
return space_above_;
}
int StructuredTable::space_below() const {
return space_below_;
}
bool StructuredTable::FindLinedStructure() {
ClearStructure();
ColPartitionGridSearch box_search(line_grid_);
box_search.SetUniqueMode(true);
box_search.StartRectSearch(bounding_box_);
ColPartition *line = nullptr;
while ((line = box_search.NextRectSearch()) != nullptr) {
if (line->IsHorizontalLine()) {
cell_y_.push_back(line->MidY());
}
if (line->IsVerticalLine()) {
cell_x_.push_back(line->MidX());
}
}
if (cell_x_.size() < 3 || cell_y_.size() < 3) {
return false;
}
std::sort(cell_x_.begin(), cell_x_.end());
auto last_x = std::unique(cell_x_.begin(), cell_x_.end());
cell_x_.erase(last_x, cell_x_.end());
std::sort(cell_y_.begin(), cell_y_.end());
auto last_y = std::unique(cell_y_.begin(), cell_y_.end());
cell_y_.erase(last_y, cell_y_.end());
cell_x_[0] = bounding_box_.left();
cell_x_[cell_x_.size() - 1] = bounding_box_.right();
cell_y_[0] = bounding_box_.bottom();
cell_y_[cell_y_.size() - 1] = bounding_box_.top();
last_x = std::unique(cell_x_.begin(), cell_x_.end());
cell_x_.erase(last_x, cell_x_.end());
last_y = std::unique(cell_y_.begin(), cell_y_.end());
cell_y_.erase(last_y, cell_y_.end());
CalculateMargins();
CalculateStats();
is_lined_ = VerifyLinedTableCells();
return is_lined_;
}
bool StructuredTable::FindWhitespacedStructure() {
ClearStructure();
FindWhitespacedColumns();
FindWhitespacedRows();
if (!VerifyWhitespacedTable()) {
return false;
} else {
bounding_box_.set_left(cell_x_[0]);
bounding_box_.set_right(cell_x_[cell_x_.size() - 1]);
bounding_box_.set_bottom(cell_y_[0]);
bounding_box_.set_top(cell_y_[cell_y_.size() - 1]);
AbsorbNearbyLines();
CalculateMargins();
CalculateStats();
return true;
}
}
bool StructuredTable::DoesPartitionFit(const ColPartition &part) const {
const TBOX &box = part.bounding_box();
for (int i : cell_x_) {
if (box.left() < i && i < box.right()) {
return false;
}
}
for (int i : cell_y_) {
if (box.bottom() < i && i < box.top()) {
return false;
}
}
return true;
}
int StructuredTable::CountFilledCells() {
return CountFilledCells(0, row_count() - 1, 0, column_count() - 1);
}
int StructuredTable::CountFilledCellsInRow(int row) {
return CountFilledCells(row, row, 0, column_count() - 1);
}
int StructuredTable::CountFilledCellsInColumn(int column) {
return CountFilledCells(0, row_count() - 1, column, column);
}
int StructuredTable::CountFilledCells(unsigned row_start, unsigned row_end, unsigned column_start,
unsigned column_end) {
ASSERT_HOST(row_start <= row_end && row_end < row_count());
ASSERT_HOST(column_start <= column_end && column_end < column_count());
int cell_count = 0;
TBOX cell_box;
for (unsigned row = row_start; row <= row_end; ++row) {
cell_box.set_bottom(cell_y_[row]);
cell_box.set_top(cell_y_[row + 1]);
for (unsigned col = column_start; col <= column_end; ++col) {
cell_box.set_left(cell_x_[col]);
cell_box.set_right(cell_x_[col + 1]);
if (CountPartitions(cell_box) > 0) {
++cell_count;
}
}
}
return cell_count;
}
bool StructuredTable::VerifyRowFilled(int row) {
for (unsigned i = 0; i < column_count(); ++i) {
auto area_filled = CalculateCellFilledPercentage(row, i);
if (area_filled >= kMinFilledArea) {
return true;
}
}
return false;
}
double StructuredTable::CalculateCellFilledPercentage(unsigned row, unsigned column) {
ASSERT_HOST(row <= row_count());
ASSERT_HOST(column <= column_count());
const TBOX kCellBox(cell_x_[column], cell_y_[row], cell_x_[column + 1], cell_y_[row + 1]);
ASSERT_HOST(!kCellBox.null_box());
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(kCellBox);
double area_covered = 0;
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (text->IsTextType()) {
area_covered += text->bounding_box().intersection(kCellBox).area();
}
}
const int32_t current_area = kCellBox.area();
if (current_area == 0) {
return 1.0;
}
return std::min(1.0, area_covered / current_area);
}
#ifndef GRAPHICS_DISABLED
void StructuredTable::Display(ScrollView *window, ScrollView::Color color) {
window->Brush(ScrollView::NONE);
window->Pen(color);
window->Rectangle(bounding_box_.left(), bounding_box_.bottom(), bounding_box_.right(),
bounding_box_.top());
for (int i : cell_x_) {
window->Line(i, bounding_box_.bottom(), i, bounding_box_.top());
}
for (int i : cell_y_) {
window->Line(bounding_box_.left(), i, bounding_box_.right(), i);
}
window->UpdateWindow();
}
#endif
void StructuredTable::ClearStructure() {
cell_x_.clear();
cell_y_.clear();
is_lined_ = false;
space_above_ = 0;
space_below_ = 0;
space_left_ = 0;
space_right_ = 0;
median_cell_height_ = 0;
median_cell_width_ = 0;
}
bool StructuredTable::VerifyLinedTableCells() {
ASSERT_HOST(cell_y_.size() >= 2 && cell_x_.size() >= 2);
for (int i : cell_y_) {
if (CountHorizontalIntersections(i) > 0) {
return false;
}
}
for (int i : cell_x_) {
if (CountVerticalIntersections(i) > 0) {
return false;
}
}
return true;
}
bool StructuredTable::VerifyWhitespacedTable() {
return row_count() >= 2 && column_count() >= 2 && cell_count() >= 6;
}
void StructuredTable::FindWhitespacedColumns() {
std::vector<int> left_sides;
std::vector<int> right_sides;
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(bounding_box_);
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (!text->IsTextType()) {
continue;
}
ASSERT_HOST(text->bounding_box().left() < text->bounding_box().right());
int spacing = static_cast<int>(text->median_width() * kHorizontalSpacing / 2.0 + 0.5);
left_sides.push_back(text->bounding_box().left() - spacing);
right_sides.push_back(text->bounding_box().right() + spacing);
}
if (left_sides.empty() || right_sides.empty()) {
return;
}
std::sort(left_sides.begin(), left_sides.end());
std::sort(right_sides.begin(), right_sides.end());
FindCellSplitLocations(left_sides, right_sides, kCellSplitColumnThreshold, &cell_x_);
}
void StructuredTable::FindWhitespacedRows() {
std::vector<int> bottom_sides;
std::vector<int> top_sides;
int min_bottom = INT32_MAX;
int max_top = INT32_MIN;
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(bounding_box_);
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (!text->IsTextType()) {
continue;
}
ASSERT_HOST(text->bounding_box().bottom() < text->bounding_box().top());
min_bottom = std::min(min_bottom, static_cast<int>(text->bounding_box().bottom()));
max_top = std::max(max_top, static_cast<int>(text->bounding_box().top()));
if (text->bounding_box().height() > max_text_height_) {
continue;
}
int spacing = static_cast<int>(text->bounding_box().height() * kVerticalSpacing / 2.0 + 0.5);
int bottom = text->bounding_box().bottom() - spacing;
int top = text->bounding_box().top() + spacing;
if (bottom >= top) {
continue;
}
bottom_sides.push_back(bottom);
top_sides.push_back(top);
}
if (bottom_sides.empty() || top_sides.empty()) {
return;
}
std::sort(bottom_sides.begin(), bottom_sides.end());
std::sort(top_sides.begin(), top_sides.end());
FindCellSplitLocations(bottom_sides, top_sides, kCellSplitRowThreshold, &cell_y_);
cell_y_[0] = min_bottom;
cell_y_[cell_y_.size() - 1] = max_top;
}
void StructuredTable::CalculateMargins() {
space_above_ = INT32_MAX;
space_below_ = INT32_MAX;
space_right_ = INT32_MAX;
space_left_ = INT32_MAX;
UpdateMargins(text_grid_);
UpdateMargins(line_grid_);
}
void StructuredTable::UpdateMargins(ColPartitionGrid *grid) {
int below = FindVerticalMargin(grid, bounding_box_.bottom(), true);
space_below_ = std::min(space_below_, below);
int above = FindVerticalMargin(grid, bounding_box_.top(), false);
space_above_ = std::min(space_above_, above);
int left = FindHorizontalMargin(grid, bounding_box_.left(), true);
space_left_ = std::min(space_left_, left);
int right = FindHorizontalMargin(grid, bounding_box_.right(), false);
space_right_ = std::min(space_right_, right);
}
int StructuredTable::FindVerticalMargin(ColPartitionGrid *grid, int border, bool decrease) const {
ColPartitionGridSearch gsearch(grid);
gsearch.SetUniqueMode(true);
gsearch.StartVerticalSearch(bounding_box_.left(), bounding_box_.right(), border);
ColPartition *part = nullptr;
while ((part = gsearch.NextVerticalSearch(decrease)) != nullptr) {
if (!part->IsTextType() && !part->IsHorizontalLine()) {
continue;
}
int distance =
decrease ? border - part->bounding_box().top() : part->bounding_box().bottom() - border;
if (distance >= 0) {
return distance;
}
}
return INT32_MAX;
}
int StructuredTable::FindHorizontalMargin(ColPartitionGrid *grid, int border, bool decrease) const {
ColPartitionGridSearch gsearch(grid);
gsearch.SetUniqueMode(true);
gsearch.StartSideSearch(border, bounding_box_.bottom(), bounding_box_.top());
ColPartition *part = nullptr;
while ((part = gsearch.NextSideSearch(decrease)) != nullptr) {
if (!part->IsTextType() && !part->IsVerticalLine()) {
continue;
}
int distance =
decrease ? border - part->bounding_box().right() : part->bounding_box().left() - border;
if (distance >= 0) {
return distance;
}
}
return INT32_MAX;
}
void StructuredTable::CalculateStats() {
const int kMaxCellHeight = 1000;
const int kMaxCellWidth = 1000;
STATS height_stats(0, kMaxCellHeight);
STATS width_stats(0, kMaxCellWidth);
for (unsigned i = 0; i < row_count(); ++i) {
height_stats.add(row_height(i), column_count());
}
for (unsigned i = 0; i < column_count(); ++i) {
width_stats.add(column_width(i), row_count());
}
median_cell_height_ = static_cast<int>(height_stats.median() + 0.5);
median_cell_width_ = static_cast<int>(width_stats.median() + 0.5);
}
void StructuredTable::AbsorbNearbyLines() {
ColPartitionGridSearch gsearch(line_grid_);
gsearch.SetUniqueMode(true);
ColPartition *line = nullptr;
gsearch.StartVerticalSearch(bounding_box_.left(), bounding_box_.right(), bounding_box_.top());
while ((line = gsearch.NextVerticalSearch(false)) != nullptr) {
if (!line->IsHorizontalLine()) {
break;
}
TBOX text_search(bounding_box_.left(), bounding_box_.top() + 1, bounding_box_.right(),
line->MidY());
if (text_search.height() > median_cell_height_ * 2) {
break;
}
if (CountPartitions(text_search) > 0) {
break;
}
bounding_box_.set_top(line->MidY());
}
line = nullptr;
gsearch.StartVerticalSearch(bounding_box_.left(), bounding_box_.right(), bounding_box_.bottom());
while ((line = gsearch.NextVerticalSearch(true)) != nullptr) {
if (!line->IsHorizontalLine()) {
break;
}
TBOX text_search(bounding_box_.left(), line->MidY(), bounding_box_.right(),
bounding_box_.bottom() - 1);
if (text_search.height() > median_cell_height_ * 2) {
break;
}
if (CountPartitions(text_search) > 0) {
break;
}
bounding_box_.set_bottom(line->MidY());
}
}
void StructuredTable::FindCellSplitLocations(const std::vector<int> &min_list,
const std::vector<int> &max_list, int max_merged,
std::vector<int> *locations) {
locations->clear();
ASSERT_HOST(min_list.size() == max_list.size());
if (min_list.empty()) {
return;
}
ASSERT_HOST(min_list.at(0) < max_list.at(0));
ASSERT_HOST(min_list.at(min_list.size() - 1) < max_list.at(max_list.size() - 1));
locations->push_back(min_list.at(0));
unsigned min_index = 0;
unsigned max_index = 0;
int stacked_partitions = 0;
int last_cross_position = INT32_MAX;
while (min_index < min_list.size()) {
if (min_list[min_index] < max_list[max_index]) {
++stacked_partitions;
if (last_cross_position != INT32_MAX && stacked_partitions > max_merged) {
int mid = (last_cross_position + min_list[min_index]) / 2;
locations->push_back(mid);
last_cross_position = INT32_MAX;
}
++min_index;
} else {
--stacked_partitions;
if (last_cross_position == INT32_MAX && stacked_partitions <= max_merged) {
last_cross_position = max_list[max_index];
}
++max_index;
}
}
locations->push_back(max_list.at(max_list.size() - 1));
}
int StructuredTable::CountVerticalIntersections(int x) {
int count = 0;
const int kGridSize = text_grid_->gridsize();
TBOX vertical_box = bounding_box_;
vertical_box.set_left(x - kGridSize);
vertical_box.set_right(x + kGridSize);
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(vertical_box);
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (!text->IsTextType()) {
continue;
}
const TBOX &box = text->bounding_box();
if (box.left() < x && x < box.right()) {
++count;
}
}
return count;
}
int StructuredTable::CountHorizontalIntersections(int y) {
int count = 0;
const int kGridSize = text_grid_->gridsize();
TBOX horizontal_box = bounding_box_;
horizontal_box.set_bottom(y - kGridSize);
horizontal_box.set_top(y + kGridSize);
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(horizontal_box);
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (!text->IsTextType()) {
continue;
}
const TBOX &box = text->bounding_box();
if (box.bottom() < y && y < box.top()) {
++count;
}
}
return count;
}
int StructuredTable::CountPartitions(const TBOX &box) {
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartRectSearch(box);
int count = 0;
ColPartition *text = nullptr;
while ((text = gsearch.NextRectSearch()) != nullptr) {
if (text->IsTextType()) {
++count;
}
}
return count;
}
void TableRecognizer::Init() {}
void TableRecognizer::set_text_grid(ColPartitionGrid *text_grid) {
text_grid_ = text_grid;
}
void TableRecognizer::set_line_grid(ColPartitionGrid *line_grid) {
line_grid_ = line_grid;
}
void TableRecognizer::set_min_height(int height) {
min_height_ = height;
}
void TableRecognizer::set_min_width(int width) {
min_width_ = width;
}
void TableRecognizer::set_max_text_height(int height) {
max_text_height_ = height;
}
StructuredTable *TableRecognizer::RecognizeTable(const TBOX &guess) {
auto *table = new StructuredTable();
table->Init();
table->set_text_grid(text_grid_);
table->set_line_grid(line_grid_);
table->set_max_text_height(max_text_height_);
if (RecognizeLinedTable(guess, table)) {
return table;
}
if (RecognizeWhitespacedTable(guess, table)) {
return table;
}
delete table;
return nullptr;
}
bool TableRecognizer::RecognizeLinedTable(const TBOX &guess_box, StructuredTable *table) {
if (!HasSignificantLines(guess_box)) {
return false;
}
TBOX line_bound = guess_box;
if (!FindLinesBoundingBox(&line_bound)) {
return false;
}
table->set_bounding_box(line_bound);
return table->FindLinedStructure();
}
bool TableRecognizer::HasSignificantLines(const TBOX &guess) {
ColPartitionGridSearch box_search(line_grid_);
box_search.SetUniqueMode(true);
box_search.StartRectSearch(guess);
ColPartition *line = nullptr;
int vertical_count = 0;
int horizontal_count = 0;
while ((line = box_search.NextRectSearch()) != nullptr) {
if (line->IsHorizontalLine()) {
++horizontal_count;
}
if (line->IsVerticalLine()) {
++vertical_count;
}
}
return vertical_count >= kLinedTableMinVerticalLines &&
horizontal_count >= kLinedTableMinHorizontalLines;
}
bool TableRecognizer::FindLinesBoundingBox(TBOX *bounding_box) {
if (!FindLinesBoundingBoxIteration(bounding_box)) {
return false;
}
bool changed = true;
while (changed) {
changed = false;
int old_area = bounding_box->area();
bool check = FindLinesBoundingBoxIteration(bounding_box);
ASSERT_HOST(check);
ASSERT_HOST(bounding_box->area() >= old_area);
changed = (bounding_box->area() > old_area);
}
return true;
}
bool TableRecognizer::FindLinesBoundingBoxIteration(TBOX *bounding_box) {
ColPartitionGridSearch box_search(line_grid_);
box_search.SetUniqueMode(true);
box_search.StartRectSearch(*bounding_box);
ColPartition *line = nullptr;
bool first_line = true;
while ((line = box_search.NextRectSearch()) != nullptr) {
if (line->IsLineType()) {
if (first_line) {
*bounding_box = line->bounding_box();
first_line = false;
} else {
*bounding_box += line->bounding_box();
}
}
}
return !first_line;
}
bool TableRecognizer::RecognizeWhitespacedTable(const TBOX &guess_box, StructuredTable *table) {
TBOX best_box = guess_box;
int best_below = 0;
int best_above = 0;
TBOX adjusted = guess_box;
const int kMidGuessY = (guess_box.bottom() + guess_box.top()) / 2;
unsigned best_cols = 0;
bool found_good_border = false;
int last_bottom = INT32_MAX;
int bottom =
NextHorizontalSplit(guess_box.left(), guess_box.right(), kMidGuessY - min_height_ / 2, true);
int top =
NextHorizontalSplit(guess_box.left(), guess_box.right(), kMidGuessY + min_height_ / 2, false);
adjusted.set_top(top);
int previous_below = 0;
const int kMaxChances = 10;
int chances = kMaxChances;
while (bottom != last_bottom) {
adjusted.set_bottom(bottom);
if (adjusted.height() >= min_height_) {
table->set_bounding_box(adjusted);
if (table->FindWhitespacedStructure() &&
table->column_count() >= best_cols * kRequiredColumns) {
if (false && IsWeakTableRow(table, 0)) {
--chances;
} else {
chances = kMaxChances;
double max_row_height = kMaxRowSize * table->median_cell_height();
if ((table->space_below() * kMarginFactor >= best_below &&
table->space_below() >= previous_below) ||
(table->CountFilledCellsInRow(0) > 1 && table->row_height(0) < max_row_height)) {
best_box.set_bottom(bottom);
best_below = table->space_below();
best_cols = std::max(table->column_count(), best_cols);
found_good_border = true;
}
}
previous_below = table->space_below();
} else {
--chances;
}
}
if (chances <= 0) {
break;
}
last_bottom = bottom;
bottom = NextHorizontalSplit(guess_box.left(), guess_box.right(), last_bottom, true);
}
if (!found_good_border) {
return false;
}
found_good_border = false;
int last_top = INT32_MIN;
top =
NextHorizontalSplit(guess_box.left(), guess_box.right(), kMidGuessY + min_height_ / 2, false);
int previous_above = 0;
chances = kMaxChances;
adjusted.set_bottom(best_box.bottom());
while (last_top != top) {
adjusted.set_top(top);
if (adjusted.height() >= min_height_) {
table->set_bounding_box(adjusted);
if (table->FindWhitespacedStructure() &&
table->column_count() >= best_cols * kRequiredColumns) {
int last_row = table->row_count() - 1;
if (false && IsWeakTableRow(table, last_row)) {
--chances;
} else {
chances = kMaxChances;
double max_row_height = kMaxRowSize * table->median_cell_height();
if ((table->space_above() * kMarginFactor >= best_above &&
table->space_above() >= previous_above) ||
(table->CountFilledCellsInRow(last_row) > 1 &&
table->row_height(last_row) < max_row_height)) {
best_box.set_top(top);
best_above = table->space_above();
best_cols = std::max(table->column_count(), best_cols);
found_good_border = true;
}
}
previous_above = table->space_above();
} else {
--chances;
}
}
if (chances <= 0) {
break;
}
last_top = top;
top = NextHorizontalSplit(guess_box.left(), guess_box.right(), last_top, false);
}
if (!found_good_border) {
return false;
}
if (best_box.null_box()) {
return false;
}
table->set_bounding_box(best_box);
return table->FindWhitespacedStructure();
}
int TableRecognizer::NextHorizontalSplit(int left, int right, int y, bool top_to_bottom) {
ColPartitionGridSearch gsearch(text_grid_);
gsearch.SetUniqueMode(true);
gsearch.StartVerticalSearch(left, right, y);
ColPartition *text = nullptr;
int last_y = y;
while ((text = gsearch.NextVerticalSearch(top_to_bottom)) != nullptr) {
if (!text->IsTextType() || !text->IsHorizontalType()) {
continue;
}
if (text->bounding_box().height() > max_text_height_) {
continue;
}
const TBOX &text_box = text->bounding_box();
if (top_to_bottom && (last_y >= y || last_y <= text_box.top())) {
last_y = std::min(last_y, static_cast<int>(text_box.bottom()));
continue;
}
if (!top_to_bottom && (last_y <= y || last_y >= text_box.bottom())) {
last_y = std::max(last_y, static_cast<int>(text_box.top()));
continue;
}
return last_y;
}
return last_y;
}
}